home *** CD-ROM | disk | FTP | other *** search
/ PC Open 101 / PC Open 101 CD 1.bin / CD1 / INTERNET / EMAIL / pop file / setup.exe / Classifier / Bayes.pm next >
Encoding:
Perl POD Document  |  2004-09-22  |  128.5 KB  |  3,856 lines

  1. # POPFILE LOADABLE MODULE
  2. package Classifier::Bayes;
  3.  
  4. use POPFile::Module;
  5. @ISA = ("POPFile::Module");
  6.  
  7. #----------------------------------------------------------------------------
  8. #
  9. # Bayes.pm --- Naive Bayes text classifier
  10. #
  11. # Copyright (c) 2001-2004 John Graham-Cumming
  12. #
  13. #   This file is part of POPFile
  14. #
  15. #   POPFile is free software; you can redistribute it and/or modify
  16. #   it under the terms of the GNU General Public License as published by
  17. #   the Free Software Foundation; either version 2 of the License, or
  18. #   (at your option) any later version.
  19. #
  20. #   POPFile is distributed in the hope that it will be useful,
  21. #   but WITHOUT ANY WARRANTY; without even the implied warranty of
  22. #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  23. #   GNU General Public License for more details.
  24. #
  25. #   You should have received a copy of the GNU General Public License
  26. #   along with POPFile; if not, write to the Free Software
  27. #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  28. #
  29. #   Modified by              Sam Schinke    (sschinke@users.sourceforge.net)
  30. #   Merged with db code from Scott Leighton (helphand@users.sourceforge.net)
  31. #
  32. #----------------------------------------------------------------------------
  33.  
  34. use strict;
  35. use warnings;
  36. use locale;
  37. use Classifier::MailParse;
  38. use IO::Handle;
  39. use DBI;
  40. use Digest::MD5 qw( md5_hex );
  41. use MIME::Base64;
  42.  
  43. # This is used to get the hostname of the current machine
  44. # in a cross platform way
  45.  
  46. use Sys::Hostname;
  47.  
  48. # A handy variable containing the value of an EOL for networks
  49.  
  50. my $eol = "\015\012";
  51.  
  52. # Korean characters definition
  53.  
  54. my $ksc5601_sym = '(?:[\xA1-\xAC][\xA1-\xFE])';
  55. my $ksc5601_han = '(?:[\xB0-\xC8][\xA1-\xFE])';
  56. my $ksc5601_hanja  = '(?:[\xCA-\xFD][\xA1-\xFE])';
  57. my $ksc5601 = "(?:$ksc5601_sym|$ksc5601_han|$ksc5601_hanja)";
  58.  
  59. my $eksc = "(?:$ksc5601|[\x81-\xC6][\x41-\xFE])"; #extended ksc
  60.  
  61. #----------------------------------------------------------------------------
  62. # new
  63. #
  64. #   Class new() function
  65. #----------------------------------------------------------------------------
  66. sub new
  67. {
  68.     my $type = shift;
  69.     my $self = POPFile::Module->new();
  70.  
  71.     # Set this to 1 to get scores for individual words in message detail
  72.  
  73.     $self->{wordscores__}        = 0;
  74.  
  75.     # Choice for the format of the "word matrix" display.
  76.  
  77.     $self->{wmformat__}          = '';
  78.  
  79.     # Just our hostname
  80.  
  81.     $self->{hostname__}        = '';
  82.  
  83.     # File Handle for DBI database
  84.  
  85.     $self->{db__}                = {};
  86.  
  87.     $self->{history__}        = 0;
  88.  
  89.     # To save time we also 'prepare' some commonly used SQL statements
  90.     # and cache them here, see the function db_connect__ for details
  91.  
  92.     $self->{db_get_buckets__} = 0;
  93.     $self->{db_get_wordid__} = 0;
  94.     $self->{db_get_word_count__} = 0;
  95.     $self->{db_put_word_count__} = 0;
  96.     $self->{db_get_bucket_unique_counts__} = 0;
  97.     $self->{db_get_unique_word_count__} = 0;
  98.     $self->{db_get_bucket_word_counts__} = 0;
  99.     $self->{db_get_full_total__} = 0;
  100.     $self->{db_get_bucket_parameter__} = 0;
  101.     $self->{db_set_bucket_parameter__} = 0;
  102.     $self->{db_get_bucket_parameter_default__} = 0;
  103.     $self->{db_get_buckets_with_magnets__} = 0;
  104.     $self->{db_delete_zero_words__} = 0;
  105.  
  106.     # Caches the name of each bucket and relates it to both the bucket
  107.     # ID in the database and whether it is pseudo or not
  108.     #
  109.     # Subkeys used are:
  110.     #
  111.     # id     The bucket ID in the database
  112.     # pseudo 1 if this is a pseudo bucket
  113.  
  114.     $self->{db_bucketid__}       = {};
  115.  
  116.     # Caches the IDs that map to parameter types
  117.  
  118.     $self->{db_parameterid__}    = {};
  119.  
  120.     # Caches looked up parameter values on a per bucket basis
  121.  
  122.     $self->{db_parameters__}     = {};
  123.  
  124.     # Used to parse mail messages
  125.     $self->{parser__}            = new Classifier::MailParse;
  126.  
  127.     # The possible colors for buckets
  128.     $self->{possible_colors__} = [ 'red',       'green',      'blue',       'brown', # PROFILE BLOCK START
  129.                                    'orange',    'purple',     'magenta',    'gray',
  130.                                    'plum',      'silver',     'pink',       'lightgreen',
  131.                                    'lightblue', 'lightcyan',  'lightcoral', 'lightsalmon',
  132.                                    'lightgrey', 'darkorange', 'darkcyan',   'feldspar',
  133.                                    'black' ];                                        # PROFILE BLOCK STOP
  134.  
  135.     # Precomputed per bucket probabilities
  136.     $self->{bucket_start__}      = {};
  137.  
  138.     # A very unlikely word
  139.     $self->{not_likely__}        = {};
  140.  
  141.     # The expected corpus version
  142.     #
  143.     # DEPRECATED  This is only used when upgrading old flat file corpus files
  144.     #             to the database
  145.     $self->{corpus_version__}    = 1;
  146.  
  147.     # The unclassified cutoff this value means that the top
  148.     # probabilily must be n times greater than the second probability,
  149.     # default is 100 times more likely
  150.     $self->{unclassified__}      = log(100);
  151.  
  152.     # Used to tell the caller whether a magnet was used in the last
  153.     # mail classification
  154.     $self->{magnet_used__}       = 0;
  155.     $self->{magnet_detail__}     = 0;
  156.  
  157.     # This maps session keys (long strings) to user ids.  If there's
  158.     # an entry here then the session key is valid and can be used in
  159.     # the POPFile API.  See the methods get_session_key and
  160.     # release_session_key for details
  161.  
  162.     $self->{api_sessions__}      = {};
  163.  
  164.     # Must call bless before attempting to call any methods
  165.  
  166.     bless $self, $type;
  167.  
  168.     $self->name( 'bayes' );
  169.  
  170.     return $self;
  171. }
  172.  
  173. #----------------------------------------------------------------------------
  174. #
  175. # forked
  176. #
  177. # This is called inside a child process that has just forked, since
  178. # the child needs access to the database we open it
  179. #
  180. #----------------------------------------------------------------------------
  181. sub forked
  182. {
  183.     my ( $self ) = @_;
  184.  
  185.     $self->db_connect__();
  186. }
  187.  
  188. #----------------------------------------------------------------------------
  189. #
  190. # initialize
  191. #
  192. # Called to set up the Bayes module's parameters
  193. #
  194. #----------------------------------------------------------------------------
  195. sub initialize
  196. {
  197.     my ( $self ) = @_;
  198.  
  199.     # This is the name for the database
  200.  
  201.     $self->config_( 'database', 'popfile.db' );
  202.  
  203.     # This is the 'connect' string used by DBI to connect to the
  204.     # database, if you decide to change from using SQLite to some
  205.     # other database (e.g. MySQL, Oracle, ... ) this *should* be all
  206.     # you need to change.  The additional parameters user and auth are
  207.     # needed for some databases.
  208.     #
  209.     # Note that the dbconnect string will be interpolated before being
  210.     # passed to DBI and the variable $dbname can be used within it and
  211.     # it resolves to the full path to the database named in the
  212.     # database parameter above.
  213.  
  214.     $self->config_( 'dbconnect', 'dbi:SQLite:dbname=$dbname' );
  215.     $self->config_( 'dbuser', '' ); $self->config_( 'dbauth', '' );
  216.     
  217.     # SQLite 1.05+ have some problems we are resolving. 
  218.     # This lets us give a nice message and then disable
  219.     # the version checking later
  220.     
  221.     $self->config_( 'bad_sqlite_version', '3.0.0' );
  222.  
  223.     # No default unclassified weight is the number of times more sure POPFile
  224.     # must be of the top class vs the second class, default is 100 times more
  225.  
  226.     $self->config_( 'unclassified_weight', 100 );
  227.  
  228.     # The corpus is kept in the 'corpus' subfolder of POPFile
  229.     #
  230.     # DEPRECATED This is only used to find an old corpus that might
  231.     # need to be upgraded
  232.  
  233.     $self->config_( 'corpus', 'corpus' );
  234.  
  235.     # The characters that appear before and after a subject modification
  236.  
  237.     $self->config_( 'subject_mod_left',  '[' );
  238.     $self->config_( 'subject_mod_right', ']' );
  239.  
  240.     # Get the hostname for use in the X-POPFile-Link header
  241.  
  242.     $self->{hostname__} = hostname;
  243.  
  244.     # Allow the user to override the hostname
  245.  
  246.     $self->config_( 'hostname', $self->{hostname__} );
  247.  
  248.     # If set to 1 then the X-POPFile-Link will have < > around the
  249.     # URL (i.e. X-POPFile-Link: <http://foo.bar>) when set to 0 there
  250.     # are none (i.e. X-POPFile-Link: http://foo.bar)
  251.  
  252.     $self->config_( 'xpl_angle', 0 );
  253.  
  254.     $self->mq_register_( 'COMIT', $self );
  255.    
  256.     $self->mq_register_( 'RELSE', $self );
  257.  
  258.     return 1;
  259. }
  260.  
  261. #----------------------------------------------------------------------------
  262. #
  263. # deliver
  264. #
  265. # Called by the message queue to deliver a message
  266. #
  267. # There is no return value from this method
  268. #
  269. #----------------------------------------------------------------------------
  270. sub deliver
  271. {
  272.     my ( $self, $type, @message ) = @_;
  273.  
  274.     if ( $type eq 'COMIT' ) {
  275.         $self->classified( $message[0], $message[2] );
  276.     }
  277.     
  278.     if ( $type eq 'RELSE' ) {
  279.         $self->release_session_key_private__( $message[0] );
  280.     }    
  281. }
  282.  
  283. #----------------------------------------------------------------------------
  284. #
  285. # start
  286. #
  287. # Called to start the Bayes module running
  288. #
  289. #----------------------------------------------------------------------------
  290. sub start
  291. {
  292.     my ( $self ) = @_;
  293.  
  294.     # In Japanese mode, explicitly set LC_COLLATE to C.  This is to
  295.     # avoid Perl crash on Windows because default LC_COLLATE of
  296.     # Japanese Win is Japanese_Japan.932(Shift_JIS), which is
  297.     # different from the charset POPFile uses for Japanese
  298.     # characters(EUC-JP).
  299.  
  300.     if ( defined( $self->module_config_( 'html', 'language' ) ) &&
  301.        ( $self->module_config_( 'html', 'language' ) eq 'Nihongo' ) ) {
  302.         use POSIX qw( locale_h );
  303.         setlocale( LC_COLLATE, 'C' );
  304.     }
  305.  
  306.     # Pass in the current interface language for language specific parsing
  307.  
  308.     $self->{parser__}->{lang__}  = $self->module_config_( 'html', 'language' );
  309.     $self->{unclassified__} = log( $self->config_( 'unclassified_weight' ) );
  310.  
  311.     if ( !$self->db_connect__() ) {
  312.         return 0;
  313.     }
  314.  
  315.     $self->upgrade_predatabase_data__();
  316.  
  317.     return 1;
  318. }
  319.  
  320. #----------------------------------------------------------------------------
  321. #
  322. # stop
  323. #
  324. # Called when POPFile is terminating
  325. #
  326. #----------------------------------------------------------------------------
  327. sub stop
  328. {
  329.     my ( $self ) = @_;
  330.  
  331.     $self->db_disconnect__();
  332. }
  333.  
  334. #----------------------------------------------------------------------------
  335. #
  336. # classified
  337. #
  338. # Called to inform the module about a classification event
  339. #
  340. # There is no return value from this method
  341. #
  342. #----------------------------------------------------------------------------
  343. sub classified
  344. {
  345.     my ( $self, $session, $class ) = @_;
  346.  
  347.     $self->set_bucket_parameter( $session, $class, 'count',             # PROFILE BLOCK START
  348.         $self->get_bucket_parameter( $session, $class, 'count' ) + 1 ); # PROFILE BLOCK STOP
  349. }
  350.  
  351. #----------------------------------------------------------------------------
  352. #
  353. # reclassified
  354. #
  355. # Called to inform the module about a reclassification from one bucket
  356. # to another
  357. #
  358. # session            Valid API session
  359. # bucket             The old bucket name
  360. # newbucket          The new bucket name
  361. # undo               1 if this is an undo operation
  362. #
  363. # There is no return value from this method
  364. #
  365. #----------------------------------------------------------------------------
  366. sub reclassified
  367. {
  368.     my ( $self, $session, $bucket, $newbucket, $undo ) = @_;
  369.  
  370.     $self->log_( 0, "Reclassification from $bucket to $newbucket" );
  371.  
  372.     my $c = $undo?-1:1;
  373.  
  374.     if ( $bucket ne $newbucket ) {
  375.         my $count = $self->get_bucket_parameter(
  376.                         $session, $newbucket, 'count' );
  377.         my $newcount = $count + $c;
  378.         $newcount = 0 if ( $newcount < 0 );
  379.         $self->set_bucket_parameter(
  380.             $session, $newbucket, 'count', $newcount );
  381.  
  382.         $count = $self->get_bucket_parameter(
  383.                      $session, $bucket, 'count' );
  384.         $newcount = $count - $c;
  385.         $newcount = 0 if ( $newcount < 0 );
  386.         $self->set_bucket_parameter(
  387.             $session, $bucket, 'count', $newcount );
  388.  
  389.         my $fncount = $self->get_bucket_parameter(
  390.                           $session, $newbucket, 'fncount' );
  391.         my $newfncount = $fncount + $c;
  392.         $newfncount = 0 if ( $newfncount < 0 );
  393.         $self->set_bucket_parameter(
  394.             $session, $newbucket, 'fncount', $newfncount );
  395.  
  396.         my $fpcount = $self->get_bucket_parameter(
  397.                           $session, $bucket, 'fpcount' );
  398.         my $newfpcount = $fpcount + $c;
  399.         $newfpcount = 0 if ( $newfpcount < 0 );
  400.         $self->set_bucket_parameter(
  401.             $session, $bucket, 'fpcount', $newfpcount );
  402.     }
  403. }
  404.  
  405. #----------------------------------------------------------------------------
  406. #
  407. # get_color
  408. #
  409. # Retrieves the color for a specific word, color is the most likely bucket
  410. #
  411. # $session  Session key returned by get_session_key
  412. # $word     Word to get the color of
  413. #
  414. #----------------------------------------------------------------------------
  415. sub get_color
  416. {
  417.     my ( $self, $session, $word ) = @_;
  418.  
  419.     my $max   = -10000;
  420.     my $color = 'black';
  421.  
  422.     for my $bucket ($self->get_buckets( $session )) {
  423.         my $prob = $self->get_value_( $session, $bucket, $word );
  424.  
  425.         if ( $prob != 0 )  {
  426.             if ( $prob > $max )  {
  427.                 $max   = $prob;
  428.                 $color = $self->get_bucket_parameter( $session, $bucket,
  429.                              'color' );
  430.             }
  431.         }
  432.     }
  433.  
  434.     return $color;
  435. }
  436.  
  437. #----------------------------------------------------------------------------
  438. #
  439. # get_not_likely_
  440. #
  441. # Returns the probability of a word that doesn't appear
  442. #
  443. #----------------------------------------------------------------------------
  444. sub get_not_likely_
  445. {
  446.     my ( $self, $session ) = @_;
  447.  
  448.     my $userid = $self->valid_session_key__( $session );
  449.     return undef if ( !defined( $userid ) );
  450.  
  451.     return $self->{not_likely__}{$userid};
  452. }
  453.  
  454. #----------------------------------------------------------------------------
  455. #
  456. # get_value_
  457. #
  458. # Returns the value for a specific word in a bucket.  The word is
  459. # converted to the log value of the probability before return to get
  460. # the raw value just hit the hash directly or call get_base_value_
  461. #
  462. #----------------------------------------------------------------------------
  463. sub get_value_
  464. {
  465.     my ( $self, $session, $bucket, $word ) = @_;
  466.  
  467.     my $value = $self->db_get_word_count__( $session, $bucket, $word );
  468.  
  469.     if ( defined( $value ) && ( $value > 0 ) ) {
  470.  
  471.         # Profiling notes:
  472.         #
  473.         # I tried caching the log of the total value and then doing
  474.         # log( $value ) - $cached and this turned out to be
  475.         # much slower than this single log with a division in it
  476.  
  477.         return log( $value /
  478.                     $self->get_bucket_word_count( $session, $bucket ) );
  479.     } else {
  480.         return 0;
  481.     }
  482. }
  483.  
  484. sub get_base_value_
  485. {
  486.     my ( $self, $session, $bucket, $word ) = @_;
  487.  
  488.     my $value = $self->db_get_word_count__( $session, $bucket, $word );
  489.  
  490.     if ( defined( $value ) ) {
  491.         return $value;
  492.     } else {
  493.         return 0;
  494.     }
  495. }
  496.  
  497. #----------------------------------------------------------------------------
  498. #
  499. # set_value_
  500. #
  501. # Sets the value for a word in a bucket and updates the total word
  502. # counts for the bucket and globally
  503. #
  504. #----------------------------------------------------------------------------
  505. sub set_value_
  506. {
  507.     my ( $self, $session, $bucket, $word, $value ) = @_;
  508.  
  509.     if ( $self->db_put_word_count__( $session, $bucket,
  510.              $word, $value ) == 1 ) {
  511.  
  512.         # If we set the word count to zero then clean it up by deleting the
  513.         # entry
  514.  
  515.         my $userid = $self->valid_session_key__( $session );
  516.         my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  517.         $self->{db_delete_zero_words__}->execute( $bucketid );
  518.  
  519.         return 1;
  520.     } else {
  521.         return 0;
  522.     }
  523. }
  524.  
  525. #----------------------------------------------------------------------------
  526. #
  527. # get_sort_value_ behaves the same as get_value_, except that it
  528. # returns not_likely__ rather than 0 if the word is not found.  This
  529. # makes its result more suitable as a sort key for bucket ranking.
  530. #
  531. #----------------------------------------------------------------------------
  532. sub get_sort_value_
  533. {
  534.     my ( $self, $session, $bucket, $word ) = @_;
  535.  
  536.     my $v = $self->get_value_( $session, $bucket, $word );
  537.  
  538.     if ( $v == 0 ) {
  539.  
  540.         my $userid = $self->valid_session_key__( $session );
  541.         return undef if ( !defined( $userid ) );
  542.  
  543.         return $self->{not_likely__}{$userid};
  544.     } else {
  545.         return $v;
  546.     }
  547. }
  548.  
  549. #----------------------------------------------------------------------------
  550. #
  551. # update_constants__
  552. #
  553. # Updates not_likely and bucket_start
  554. #
  555. #----------------------------------------------------------------------------
  556. sub update_constants__
  557. {
  558.     my ( $self, $session ) = @_;
  559.  
  560.     my $wc = $self->get_word_count( $session );
  561.  
  562.     my $userid = $self->valid_session_key__( $session );
  563.     return undef if ( !defined( $userid ) );
  564.  
  565.     if ( $wc > 0 )  {
  566.         $self->{not_likely__}{$userid} = -log( 10 * $wc );
  567.  
  568.         foreach my $bucket ($self->get_buckets( $session )) {
  569.             my $total = $self->get_bucket_word_count( $session, $bucket );
  570.  
  571.             if ( $total != 0 ) {
  572.                 $self->{bucket_start__}{$userid}{$bucket} = log( $total /
  573.                                                                  $wc );
  574.             } else {
  575.                 $self->{bucket_start__}{$userid}{$bucket} = 0;
  576.             }
  577.         }
  578.     } else {
  579.         $self->{not_likely__}{$userid} = 0;
  580.     }
  581. }
  582.  
  583. #----------------------------------------------------------------------------
  584. #
  585. # db_connect__
  586. #
  587. # Connects to the POPFile database and returns 1 if successful
  588. #
  589. #----------------------------------------------------------------------------
  590. sub db_connect__
  591. {
  592.     my ( $self ) = @_;
  593.  
  594.     # Connect to the database, note that the database must exist for
  595.     # this to work, to make this easy for people POPFile we will
  596.     # create the database automatically here using the file
  597.     # 'popfile.sql' which should be located in the same directory the
  598.     # Classifier/Bayes.pm module
  599.  
  600.     # If we are using SQLite then the dbname is actually the name of a
  601.     # file, and hence we treat it like one, otherwise we leave it
  602.     # alone
  603.  
  604.     my $dbname;
  605.     my $dbconnect = $self->config_( 'dbconnect' );
  606.     my $dbpresent;
  607.     my $sqlite = ( $dbconnect =~ /sqlite/i );
  608.  
  609.     if ( $sqlite ) {
  610.         $dbname = $self->get_user_path_( $self->config_( 'database' ) );
  611.         $dbpresent = ( -e $dbname ) || 0;                
  612.     } else {
  613.         $dbname = $self->config_( 'database' );
  614.         $dbpresent = 1;
  615.     }
  616.  
  617.     # Now perform the connect, note that this is database independent
  618.     # at this point, the actual database that we connect to is defined
  619.     # by the dbconnect parameter.
  620.  
  621.     $dbconnect =~ s/\$dbname/$dbname/g;
  622.  
  623.     $self->log_( 0, "Attempting to connect to $dbconnect ($dbpresent)" );
  624.  
  625.     $self->{db__} = DBI->connect( $dbconnect,                    # PROFILE BLOCK START
  626.                                   $self->config_( 'dbuser' ),
  627.                                   $self->config_( 'dbauth' ) );  # PROFILE BLOCK STOP
  628.                                   
  629.     $self->log_( 0, "Using SQLite library version " . $self->{db__}{sqlite_version});
  630.     
  631.     # We check to make sure we're not using DBD::SQLite 1.05 or greater
  632.     # which uses SQLite V 3 If so, we'll use DBD::SQLite2 and SQLite 2.8,
  633.     # which is still compatible with old databases
  634.     
  635.     if ( $self->{db__}{sqlite_version} gt $self->config_('bad_sqlite_version' ) )  {
  636.         $self->log_( 0, "Substituting DBD::SQLite2 for DBD::SQLite 1.05" );        
  637.         $self->log_( 0, "Please install DBD::SQLite2 and set dbconnect to use DBD::SQLite2" );
  638.         
  639.         $dbconnect =~ s/SQLite/SQLite2/;
  640.         
  641.         undef $self->{db__};
  642. #         $self->db_disconnect__();
  643.         
  644.         $self->{db__} = DBI->connect( $dbconnect,                    # PROFILE BLOCK START
  645.                                       $self->config_( 'dbuser' ),
  646.                                       $self->config_( 'dbauth' ) );  # PROFILE BLOCK STOP        
  647.     }
  648.  
  649.     if ( !defined( $self->{db__} ) ) {
  650.         $self->log_( 0, "Failed to connect to database and got error $DBI::errstr" );
  651.         return 0;
  652.     }
  653.  
  654.     if ( !$dbpresent ) {
  655.         if ( !$self->insert_schema__( $sqlite ) ) {
  656.             return 0;
  657.         }
  658.     }
  659.  
  660.     # Now check for a need to upgrade the database because the schema
  661.     # has been changed.  From POPFile v0.22.0 there's a special
  662.     # 'popfile' table inside the database that contains the schema
  663.     # version number.  If the version number doesn't match or is
  664.     # missing then do the upgrade.
  665.  
  666.     open SCHEMA, '<' . $self->get_root_path_( 'Classifier/popfile.sql' );
  667.     <SCHEMA> =~ /-- POPFILE SCHEMA (\d+)/;
  668.     my $version = $1;
  669.     close SCHEMA;
  670.  
  671.     my $need_upgrade = 1;
  672.  
  673.     #
  674.     # retrieve the SQL_IDENTIFIER_QUOTE_CHAR for the database then use it
  675.     # to strip off any sqlquotechars from the table names we retrieve
  676.     #
  677.  
  678.     my $sqlquotechar = $self->{db__}->get_info(29) || ''; 
  679.     my @tables = map { s/$sqlquotechar//g; $_ } ($self->{db__}->tables());
  680.  
  681.     foreach my $table (@tables) {
  682.         if ( $table eq 'popfile' ) {
  683.             my @row = $self->{db__}->selectrow_array(
  684.                'select version from popfile;' );
  685.  
  686.             if ( $#row == 0 ) {
  687.                 $need_upgrade = ( $row[0] != $version );
  688.             }
  689.         }
  690.     }
  691.  
  692.     if ( $need_upgrade ) {
  693.  
  694.         print "\n\nDatabase schema is outdated, performing automatic upgrade\n";
  695.         # The database needs upgrading, so we are going to dump out
  696.         # all the data in the database as INSERT statements in a
  697.         # temporary file, then DROP all the tables in the database,
  698.         # then recreate the schema from the new schema and finally
  699.         # rerun the inserts.
  700.  
  701.         my $i = 0;
  702.         my $ins_file = $self->get_user_path_( 'insert.sql' );
  703.         open INSERT, '>' . $ins_file;
  704.  
  705.         foreach my $table (@tables) {
  706.             next if ( $table eq 'popfile' );
  707.             if ( $sqlite && ( $table =~ /^sqlite_/ ) ) {
  708.                 next;
  709.             }
  710.             if ( $i > 99 ) {
  711.                 print "\n";
  712.             }
  713.             print "    Saving table $table\n    ";
  714.  
  715.             my $t = $self->{db__}->prepare( "select * from $table;" );
  716.             $t->execute;
  717.             $i = 0;
  718.             while ( 1 ) {
  719.                 if ( ( ++$i % 100 ) == 0 ) {
  720.                     print "[$i]";
  721.                     flush STDOUT;
  722.                 }
  723.                 my @rows = $t->fetchrow_array;
  724.  
  725.                 last if ( $#rows == -1 );
  726.  
  727.                 print INSERT "INSERT INTO $table (";
  728.                 for my $i (0..$t->{NUM_OF_FIELDS}-1) {
  729.                     if ( $i != 0 ) {
  730.                         print INSERT ',';
  731.                     }
  732.                     print INSERT $t->{NAME}->[$i];
  733.                 }
  734.                 print INSERT ') VALUES (';
  735.                 for my $i (0..$t->{NUM_OF_FIELDS}-1) {
  736.                     if ( $i != 0 ) {
  737.                         print INSERT ',';
  738.                     }
  739.                     my $val = $rows[$i];
  740.                     if ( $t->{TYPE}->[$i] !~ /^int/i ) {
  741.                         $val = '' if ( !defined( $val ) );
  742.                         $val = $self->{db__}->quote( $val );
  743.                     } else {
  744.                         $val = 'NULL' if ( !defined( $val ) );
  745.                     }
  746.                     print INSERT $val;
  747.                 }
  748.                 print INSERT ");\n";
  749.             }
  750.         }
  751.  
  752.         close INSERT;
  753.  
  754.         if ( $i > 99 ) {
  755.             print "\n";
  756.         }
  757.  
  758.         foreach my $table (@tables) {
  759.             if ( $sqlite && ( $table =~ /^sqlite_/ ) ) {
  760.                 next;
  761.             }
  762.             print "    Dropping old table $table\n";
  763.             $self->{db__}->do( "DROP TABLE $table;" );
  764.         }
  765.  
  766.         print "    Inserting new database schema\n";
  767.         if ( !$self->insert_schema__( $sqlite ) ) {
  768.             return 0;
  769.         }
  770.  
  771.         print "    Restoring old data\n    ";
  772.  
  773.         $self->{db__}->begin_work;
  774.         open INSERT, '<' . $ins_file;
  775.         $i = 0;
  776.         while ( <INSERT> ) {
  777.             if ( ( ++$i % 100 ) == 0 ) {
  778.                print "[$i]";
  779.                flush STDOUT;
  780.             }
  781.             s/[\r\n]//g;
  782.             $self->{db__}->do( $_ );
  783.         }
  784.         close INSERT;
  785.         $self->{db__}->commit;
  786.  
  787.         unlink $ins_file;
  788.         print "\nDatabase upgrade complete\n\n";
  789.     }
  790.  
  791.     # Now prepare common SQL statements for use, as a matter of convention the
  792.     # parameters to each statement always appear in the following order:
  793.     #
  794.     # user
  795.     # bucket
  796.     # word
  797.     # parameter
  798.  
  799.     $self->{db_get_buckets__} = $self->{db__}->prepare(                                 # PROFILE BLOCK START
  800.              'select name, id, pseudo from buckets
  801.                   where buckets.userid = ?;' );                                         # PROFILE BLOCK STOP
  802.  
  803.     $self->{db_get_wordid__} = $self->{db__}->prepare(                                  # PROFILE BLOCK START
  804.              'select id from words
  805.                   where words.word = ? limit 1;' );                                     # PROFILE BLOCK STOP
  806.  
  807.     $self->{db_get_userid__} = $self->{db__}->prepare(                                  # PROFILE BLOCK START
  808.              'select id from users where name = ?
  809.                                      and password = ? limit 1;' );                      # PROFILE BLOCK STOP
  810.  
  811.     $self->{db_get_word_count__} = $self->{db__}->prepare(                              # PROFILE BLOCK START
  812.              'select matrix.times from matrix
  813.                   where matrix.bucketid = ? and
  814.                         matrix.wordid = ? limit 1;' );                                  # PROFILE BLOCK STOP
  815.  
  816.     $self->{db_put_word_count__} = $self->{db__}->prepare(                              # PROFILE BLOCK START
  817.            'replace into matrix ( bucketid, wordid, times ) values ( ?, ?, ? );' );     # PROFILE BLOCK STOP
  818.  
  819.     $self->{db_get_bucket_unique_counts__} = $self->{db__}->prepare(                    # PROFILE BLOCK START
  820.              'select count(matrix.wordid), buckets.name from matrix, buckets
  821.                   where buckets.userid = ?
  822.                     and matrix.bucketid = buckets.id
  823.                   group by buckets.name;' );                                            # PROFILE BLOCK STOP
  824.  
  825.     $self->{db_get_bucket_word_counts__} = $self->{db__}->prepare(                      # PROFILE BLOCK START
  826.              'select sum(matrix.times), buckets.name from matrix, buckets
  827.                   where matrix.bucketid = buckets.id
  828.                     and buckets.userid = ?
  829.                     group by buckets.name;' );                                          # PROFILE BLOCK STOP
  830.  
  831.     $self->{db_get_unique_word_count__} = $self->{db__}->prepare(                       # PROFILE BLOCK START
  832.              'select count(matrix.wordid) from matrix, buckets
  833.                   where matrix.bucketid = buckets.id and
  834.                         buckets.userid = ?;' );                                         # PROFILE BLOCK STOP
  835.  
  836.     $self->{db_get_full_total__} = $self->{db__}->prepare(                              # PROFILE BLOCK START
  837.              'select sum(matrix.times) from matrix, buckets
  838.                   where buckets.userid = ? and
  839.                         matrix.bucketid = buckets.id;' );                               # PROFILE BLOCK STOP
  840.  
  841.     $self->{db_get_bucket_parameter__} = $self->{db__}->prepare(                        # PROFILE BLOCK START
  842.              'select bucket_params.val from bucket_params
  843.                   where bucket_params.bucketid = ? and
  844.                         bucket_params.btid = ?;' );                                     # PROFILE BLOCK STOP
  845.  
  846.     $self->{db_set_bucket_parameter__} = $self->{db__}->prepare(                        # PROFILE BLOCK START
  847.            'replace into bucket_params ( bucketid, btid, val ) values ( ?, ?, ? );' );  # PROFILE BLOCK STOP
  848.  
  849.     $self->{db_get_bucket_parameter_default__} = $self->{db__}->prepare(                # PROFILE BLOCK START
  850.              'select bucket_template.def from bucket_template
  851.                   where bucket_template.id = ?;' );                                     # PROFILE BLOCK STOP
  852.  
  853.     $self->{db_get_buckets_with_magnets__} = $self->{db__}->prepare(                    # PROFILE BLOCK START
  854.              'select buckets.name from buckets, magnets
  855.                   where buckets.userid = ? and
  856.                         magnets.id != 0 and
  857.                         magnets.bucketid = buckets.id group by buckets.name order by buckets.name;' );
  858.                                                                                         # PROFILE BLOCK STOP
  859.     $self->{db_delete_zero_words__} = $self->{db__}->prepare(                           # PROFILE BLOCK START
  860.              'delete from matrix
  861.                   where matrix.times = 0
  862.                     and matrix.bucketid = ?;' );                                        # PROFILE BLOCK STOP
  863.  
  864.     # Get the mapping from parameter names to ids into a local hash
  865.  
  866.     my $h = $self->{db__}->prepare( "select name, id from bucket_template;" );
  867.     $h->execute;
  868.     while ( my $row = $h->fetchrow_arrayref ) {
  869.         $self->{db_parameterid__}{$row->[0]} = $row->[1];
  870.     }
  871.     $h->finish;
  872.  
  873.     return 1;
  874. }
  875.  
  876. #----------------------------------------------------------------------------
  877. #
  878. # insert_schema__
  879. #
  880. # Insert the POPFile schema in a database
  881. #
  882. # $sqlite          Set to 1 if this is a SQLite database
  883. #
  884. #----------------------------------------------------------------------------
  885. sub insert_schema__
  886. {
  887.     my ( $self, $sqlite ) = @_;
  888.  
  889.     if ( -e $self->get_root_path_( 'Classifier/popfile.sql' ) ) {
  890.         my $schema = '';
  891.  
  892.         $self->log_( 0, "Creating database schema" );
  893.  
  894.         open SCHEMA, '<' . $self->get_root_path_( 'Classifier/popfile.sql' );
  895.         while ( <SCHEMA> ) {
  896.             next if ( /^--/ );
  897.             next if ( !/[a-z;]/ );
  898.             s/--.*$//;
  899.  
  900.             # If the line begins 'alter' and we are doing SQLite then ignore
  901.             # the line
  902.  
  903.             if ( $sqlite && ( /^alter/i ) ) {
  904.                 next;
  905.             }
  906.  
  907.             $schema .= $_;
  908.  
  909.             if ( ( /end;/ ) || ( /\);/ ) || ( /^alter/i ) ) {
  910.                 $self->{db__}->do( $schema );
  911.                 $schema = '';
  912.             }
  913.         }
  914.         close SCHEMA;
  915.         return 1;
  916.     } else {
  917.         $self->log_( 0, "Can't find the database schema" );
  918.         return 0;
  919.     }
  920. }
  921.  
  922. #----------------------------------------------------------------------------
  923. #
  924. # db_disconnect__
  925. #
  926. # Disconnect from the POPFile database
  927. #
  928. #----------------------------------------------------------------------------
  929. sub db_disconnect__
  930. {
  931.     my ( $self ) = @_;
  932.  
  933.     $self->{db_get_buckets__}->finish;
  934.     $self->{db_get_wordid__}->finish;
  935.     $self->{db_get_userid__}->finish;
  936.     $self->{db_get_word_count__}->finish;
  937.     $self->{db_put_word_count__}->finish;
  938.     $self->{db_get_bucket_unique_counts__}->finish;
  939.     $self->{db_get_bucket_word_counts__}->finish;
  940.     $self->{db_get_unique_word_count__}->finish;
  941.     $self->{db_get_full_total__}->finish;
  942.     $self->{db_get_bucket_parameter__}->finish;
  943.     $self->{db_set_bucket_parameter__}->finish;
  944.     $self->{db_get_bucket_parameter_default__}->finish;
  945.     $self->{db_get_buckets_with_magnets__}->finish;
  946.     $self->{db_delete_zero_words__}->finish;
  947.  
  948.     if ( defined( $self->{db__} ) ) {
  949.         $self->{db__}->disconnect;
  950.         undef $self->{db__};
  951.     }
  952. }
  953.  
  954. #----------------------------------------------------------------------------
  955. #
  956. # db_update_cache__
  957. #
  958. # Updates our local cache of user and bucket ids.
  959. #
  960. # $session           Must be a valid session
  961. #
  962. #----------------------------------------------------------------------------
  963. sub db_update_cache__
  964. {
  965.     my ( $self, $session ) = @_;
  966.  
  967.     my $userid = $self->valid_session_key__( $session );
  968.     return undef if ( !defined( $userid ) );
  969.  
  970.     delete $self->{db_bucketid__}{$userid};
  971.  
  972.     $self->{db_get_buckets__}->execute( $userid );
  973.     while ( my $row = $self->{db_get_buckets__}->fetchrow_arrayref ) {
  974.         $self->{db_bucketid__}{$userid}{$row->[0]}{id} = $row->[1];
  975.         $self->{db_bucketid__}{$userid}{$row->[0]}{pseudo} = $row->[2];
  976.         $self->{db_bucketcount__}{$userid}{$row->[0]} = 0;
  977.     }
  978.  
  979.     $self->{db_get_bucket_word_counts__}->execute( $userid );
  980.  
  981.     for my $b (sort keys %{$self->{db_bucketid__}{$userid}}) {
  982.         $self->{db_bucketcount__}{$userid}{$b} = 0;
  983.         $self->{db_bucketunique__}{$userid}{$b} = 0;
  984.     }
  985.  
  986.     while ( my $row = $self->{db_get_bucket_word_counts__}->fetchrow_arrayref ) {
  987.         $self->{db_bucketcount__}{$userid}{$row->[1]} = $row->[0];
  988.     }
  989.  
  990.     $self->{db_get_bucket_unique_counts__}->execute( $userid );
  991.  
  992.     while ( my $row = $self->{db_get_bucket_unique_counts__}->fetchrow_arrayref ) {
  993.         $self->{db_bucketunique__}{$userid}{$row->[1]} = $row->[0];
  994.     }
  995.  
  996.     $self->update_constants__( $session );
  997. }
  998.  
  999. #----------------------------------------------------------------------------
  1000. #
  1001. # db_get_word_count__
  1002. #
  1003. # Return the 'count' value for a word in a bucket.  If the word is not
  1004. # found in that bucket then returns undef.
  1005. #
  1006. # $session          Valid session ID from get_session_key
  1007. # $bucket           bucket word is in
  1008. # $word             word to lookup
  1009. #
  1010. #----------------------------------------------------------------------------
  1011. sub db_get_word_count__
  1012. {
  1013.     my ( $self, $session, $bucket, $word ) = @_;
  1014.  
  1015.     my $userid = $self->valid_session_key__( $session );
  1016.     return undef if ( !defined( $userid ) );
  1017.  
  1018.     $self->{db_get_wordid__}->execute( $word );
  1019.     my $result = $self->{db_get_wordid__}->fetchrow_arrayref;
  1020.     if ( !defined( $result ) ) {
  1021.         return undef;
  1022.     }
  1023.  
  1024.     my $wordid = $result->[0];
  1025.  
  1026.     $self->{db_get_word_count__}->execute( $self->{db_bucketid__}{$userid}{$bucket}{id}, $wordid );
  1027.     $result = $self->{db_get_word_count__}->fetchrow_arrayref;
  1028.     if ( defined( $result ) ) {
  1029.          return $result->[0];
  1030.     } else {
  1031.          return undef;
  1032.     }
  1033. }
  1034.  
  1035. #----------------------------------------------------------------------------
  1036. #
  1037. # db_put_word_count__
  1038. #
  1039. # Update 'count' value for a word in a bucket, if the update fails
  1040. # then returns 0 otherwise is returns 1
  1041. #
  1042. # $session          Valid session ID from get_session_key
  1043. # $bucket           bucket word is in
  1044. # $word             word to update
  1045. # $count            new count value
  1046. #
  1047. #----------------------------------------------------------------------------
  1048. sub db_put_word_count__
  1049. {
  1050.     my ( $self, $session, $bucket, $word, $count ) = @_;
  1051.  
  1052.     my $userid = $self->valid_session_key__( $session );
  1053.     return undef if ( !defined( $userid ) );
  1054.  
  1055.     # We need to have two things before we can start, the id of the
  1056.     # word in the words table (if there's none then we need to add the
  1057.     # word), the bucket id in the buckets table (which must exist)
  1058.  
  1059.     $word = $self->{db__}->quote($word);
  1060.  
  1061.     my $result = $self->{db__}->selectrow_arrayref(
  1062.                      "select words.id from words where words.word = $word limit 1;");
  1063.  
  1064.     if ( !defined( $result ) ) {
  1065.         $self->{db__}->do( "insert into words ( word ) values ( $word );" );
  1066.         $result = $self->{db__}->selectrow_arrayref(
  1067.                      "select words.id from words where words.word = $word limit 1;");
  1068.     }
  1069.  
  1070.     my $wordid = $result->[0];
  1071.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  1072.  
  1073.     $self->{db_put_word_count__}->execute( $bucketid, $wordid, $count );
  1074.  
  1075.     return 1;
  1076. }
  1077.  
  1078. #----------------------------------------------------------------------------
  1079. #
  1080. # upgrade_predatabase_data__
  1081. #
  1082. # Looks for old POPFile data (in flat files or BerkeleyDB tables) and
  1083. # upgrades it to the SQL database.  Data upgraded is removed.
  1084. #
  1085. #----------------------------------------------------------------------------
  1086. sub upgrade_predatabase_data__
  1087. {
  1088.     my ( $self ) = @_;
  1089.     my $c      = 0;
  1090.  
  1091.     # There's an assumption here that this is the single user version
  1092.     # of POPFile and hence what we do is cheat and get a session key
  1093.     # assuming that the user name is admin with password ''
  1094.  
  1095.     my $session = $self->get_session_key( 'admin', '' );
  1096.  
  1097.     if ( !defined( $session ) ) {
  1098.         $self->log_( 0, "Tried to get the session key for user admin and failed; cannot upgrade old data" );
  1099.         return;
  1100.     }
  1101.  
  1102.     my @buckets = glob $self->get_user_path_( $self->config_( 'corpus' ) . '/*' );
  1103.  
  1104.     foreach my $bucket (@buckets) {
  1105.  
  1106.         # A bucket directory must be a directory
  1107.  
  1108.         next unless ( -d $bucket );
  1109.         next unless ( ( -e "$bucket/table" ) || ( -e "$bucket/table.db" ) );
  1110.  
  1111.         return 0 if ( !$self->upgrade_bucket__( $session, $bucket ) );
  1112.  
  1113.         my $color = '';
  1114.  
  1115.         # See if there's a color file specified
  1116.         if ( open COLOR, '<' . "$bucket/color" ) {
  1117.             $color = <COLOR>;
  1118.  
  1119.             # Someone (who shall remain nameless) went in and manually created
  1120.             # empty color files in their corpus directories which would cause
  1121.             # $color at this point to be undefined and hence you'd get warnings
  1122.             # about undefined variables below.  So this little test is to deal
  1123.             # with that user and to make POPFile a little safer which is always
  1124.             # a good thing
  1125.  
  1126.             if ( !defined( $color ) ) {
  1127.                 $color = '';
  1128.             } else {
  1129.                 $color =~ s/[\r\n]//g;
  1130.             }
  1131.             close COLOR;
  1132.             unlink "$bucket/color";
  1133.         }
  1134.  
  1135.         $bucket =~ /([[:alpha:]0-9-_]+)$/;
  1136.         $bucket =  $1;
  1137.  
  1138.         $self->set_bucket_color( $session, $bucket, ($color eq '')?$self->{possible_colors__}[$c]:$color );
  1139.  
  1140.         $c = ($c+1) % ($#{$self->{possible_colors__}}+1);
  1141.     }
  1142.  
  1143.     $self->release_session_key( $session );
  1144.  
  1145.     return 1;
  1146. }
  1147.  
  1148. #----------------------------------------------------------------------------
  1149. #
  1150. # upgrade_bucket__
  1151. #
  1152. # Loads an individual bucket
  1153. #
  1154. # $session           Valid session key from get_session_key
  1155. # $bucket            The bucket name
  1156. #
  1157. #----------------------------------------------------------------------------
  1158. sub upgrade_bucket__
  1159. {
  1160.     my ( $self, $session, $bucket ) = @_;
  1161.  
  1162.     $bucket =~ /([[:alpha:]0-9-_]+)$/;
  1163.     $bucket =  $1;
  1164.  
  1165.     $self->create_bucket( $session, $bucket );
  1166.  
  1167.     if ( open PARAMS, '<' . $self->get_user_path_( $self->config_( 'corpus' ) . "/$bucket/params" ) ) {
  1168.         while ( <PARAMS> )  {
  1169.             s/[\r\n]//g;
  1170.             if ( /^([[:lower:]]+) ([^\r\n\t ]+)$/ )  {
  1171.                 $self->set_bucket_parameter( $session, $bucket, $1, $2 );
  1172.             }
  1173.         }
  1174.         close PARAMS;
  1175.         unlink $self->get_user_path_( $self->config_( 'corpus' ) . "/$bucket/params" );
  1176.     }
  1177.  
  1178.     # Pre v0.21.0 POPFile had GLOBAL parameters for subject modification,
  1179.     # XTC and XPL insertion.  To make the upgrade as clean as possible
  1180.     # check these parameters so that if they were OFF we set the equivalent
  1181.     # per bucket to off
  1182.  
  1183.     foreach my $gl ( 'subject', 'xtc', 'xpl' ) {
  1184.         $self->log_( 1, "Checking deprecated parameter GLOBAL_$gl for $bucket\n" );
  1185.         my $val = $self->{configuration__}->deprecated_parameter( "GLOBAL_$gl" );
  1186.         if ( defined( $val ) && ( $val == 0 ) ) {
  1187.             $self->log_( 1, "GLOBAL_$gl is 0 for $bucket, overriding $gl\n" );
  1188.             $self->set_bucket_parameter( $session, $bucket, $gl, 0 );
  1189.         }
  1190.     }
  1191.  
  1192.     # See if there are magnets defined
  1193.     if ( open MAGNETS, '<' . $self->get_user_path_( $self->config_( 'corpus' ) . "/$bucket/magnets" ) ) {
  1194.         while ( <MAGNETS> )  {
  1195.             s/[\r\n]//g;
  1196.  
  1197.             # Because of a bug in v0.17.9 and earlier of POPFile the text of
  1198.             # some magnets was getting mangled by certain characters having
  1199.             # a \ prepended.  Code here removes the \ in these cases to make
  1200.             # an upgrade smooth.
  1201.  
  1202.             if ( /^([^ ]+) (.+)$/ )  {
  1203.                 my $type  = $1;
  1204.                 my $value = $2;
  1205.  
  1206.                 # Some people were accidently creating magnets with
  1207.                 # trailing whitespace which really confused them later
  1208.                 # when their magnet did not match (see comment in
  1209.                 # UI::HTML::magnet for more detail)
  1210.  
  1211.                 $value =~ s/^[ \t]+//g;
  1212.                 $value =~ s/[ \t]+$//g;
  1213.  
  1214.                 $value =~ s/\\(\?|\*|\||\(|\)|\[|\]|\{|\}|\^|\$|\.)/$1/g;
  1215.                 $self->create_magnet( $session, $bucket, $type, $value );
  1216.             } else {
  1217.  
  1218.                 # This branch is used to catch the original magnets in an
  1219.                 # old version of POPFile that were just there for from
  1220.                 # addresses only
  1221.  
  1222.                 if ( /^(.+)$/ ) {
  1223.                     my $value = $1;
  1224.                     $value =~ s/\\(\?|\*|\||\(|\)|\[|\]|\{|\}|\^|\$|\.)/$1/g;
  1225.                     $self->create_magnet( $session, $bucket, 'from', $value );
  1226.                 }
  1227.             }
  1228.         }
  1229.         close MAGNETS;
  1230.         unlink $self->get_user_path_( $self->config_( 'corpus' ) . "/$bucket/magnets" );
  1231.     }
  1232.  
  1233.     # If there is no existing table but there is a table file (the old style
  1234.     # flat file used by POPFile for corpus storage) then create the new
  1235.     # database from it thus performing an automatic upgrade.
  1236.  
  1237.     if ( -e $self->get_user_path_( $self->config_( 'corpus' ) . "/$bucket/table" ) ) {
  1238.         $self->log_( 0, "Performing automatic upgrade of $bucket corpus from flat file to DBI" );
  1239.  
  1240.         $self->{db__}->begin_work;
  1241.  
  1242.         if ( open WORDS, '<' . $self->get_user_path_( $self->config_( 'corpus' ) . "/$bucket/table" ) )  {
  1243.  
  1244.             my $wc = 1;
  1245.  
  1246.             my $first = <WORDS>;
  1247.             if ( defined( $first ) && ( $first =~ s/^__CORPUS__ __VERSION__ (\d+)// ) ) {
  1248.                 if ( $1 != $self->{corpus_version__} )  {
  1249.                     print STDERR "Incompatible corpus version in $bucket\n";
  1250.                     close WORDS;
  1251.                     $self->{db__}->rollback;
  1252.                     return 0;
  1253.                 } else {
  1254.                     $self->log_( 0, "Upgrading bucket $bucket..." );
  1255.  
  1256.                     while ( <WORDS> ) {
  1257.                         if ( $wc % 100 == 0 ) {
  1258.                             $self->log_( 0, "$wc" );
  1259.                         }
  1260.                         $wc += 1;
  1261.                         s/[\r\n]//g;
  1262.  
  1263.                         if ( /^([^\s]+) (\d+)$/ ) {
  1264.                             if ( $2 != 0 ) {
  1265.                                 $self->db_put_word_count__( $session, $bucket, $1, $2 );
  1266.                             }
  1267.                         } else {
  1268.                             $self->log_( 0, "Found entry in corpus for $bucket that looks wrong: \"$_\" (ignoring)" );
  1269.                         }
  1270.                     }
  1271.                 }
  1272.  
  1273.                 if ( $wc > 1 ) {
  1274.                     $wc -= 1;
  1275.                     $self->log_( 0, "(completed $wc words)" );
  1276.                 }
  1277.                 close WORDS;
  1278.             } else {
  1279.                 close WORDS;
  1280.                 $self->{db__}->rollback;
  1281.                 unlink $self->get_user_path_( $self->config_( 'corpus' ) . "/$bucket/table" );
  1282.                 return 0;
  1283.             }
  1284.  
  1285.             $self->{db__}->commit;
  1286.             unlink $self->get_user_path_( $self->config_( 'corpus' ) . "/$bucket/table" );
  1287.         }
  1288.     }
  1289.  
  1290.     # Now check to see if there's a BerkeleyDB-style table
  1291.  
  1292.     my $bdb_file = $self->get_user_path_( $self->config_( 'corpus' ) . "/$bucket/table.db" );
  1293.  
  1294.     if ( -e $bdb_file ) {
  1295.         $self->log_( 0, "Performing automatic upgrade of $bucket corpus from BerkeleyDB to DBI" );
  1296.  
  1297.         require BerkeleyDB;
  1298.  
  1299.         my %h;
  1300.         tie %h, "BerkeleyDB::Hash", -Filename => $bdb_file;
  1301.  
  1302.         $self->log_( 0, "Upgrading bucket $bucket..." );
  1303.         $self->{db__}->begin_work;
  1304.  
  1305.         my $wc = 1;
  1306.  
  1307.         for my $word (keys %h) {
  1308.             if ( $wc % 100 == 0 ) {
  1309.                 $self->log_( 0, "$wc" );
  1310.             }
  1311.  
  1312.             next if ( $word =~ /__POPFILE__(LOG__TOTAL|TOTAL|UNIQUE)__/ );
  1313.  
  1314.             $wc += 1;
  1315.             if ( $h{$word} != 0 ) {
  1316.                 $self->db_put_word_count__( $session, $bucket, $word, $h{$word} );
  1317.             }
  1318.         }
  1319.  
  1320.         $wc -= 1;
  1321.         $self->log_( 0, "(completed $wc words)" );
  1322.         $self->{db__}->commit;
  1323.         untie %h;
  1324.         unlink $bdb_file;
  1325.     }
  1326.  
  1327.     return 1;
  1328. }
  1329.  
  1330. #----------------------------------------------------------------------------
  1331. #
  1332. # magnet_match_helper__
  1333. #
  1334. # Helper the determines if a specific string matches a certain magnet
  1335. # type in a bucket, used by magnet_match_
  1336. #
  1337. # $session         Valid session from get_session_key
  1338. # $match           The string to match
  1339. # $bucket          The bucket to check
  1340. # $type            The magnet type to check
  1341. #
  1342. #----------------------------------------------------------------------------
  1343. sub magnet_match_helper__
  1344. {
  1345.     my ( $self, $session, $match, $bucket, $type ) = @_;
  1346.  
  1347.     my $userid = $self->valid_session_key__( $session );
  1348.     return undef if ( !defined( $userid ) );
  1349.  
  1350.     $match = lc($match);
  1351.  
  1352.     # In Japanese and Korean mode, disable locale.  Sorting Japanese
  1353.     # and Korean with "use locale" is memory and time consuming, and
  1354.     # may cause perl crash.
  1355.  
  1356.     my @magnets;
  1357.  
  1358.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  1359.     my $h = $self->{db__}->prepare(                                           # PROFILE BLOCK START
  1360.         "select magnets.val, magnets.id from magnets, users, buckets, magnet_types
  1361.              where buckets.id = $bucketid and
  1362.                    magnets.id != 0 and
  1363.                    users.id = buckets.userid and
  1364.                    magnets.bucketid = buckets.id and
  1365.                    magnet_types.mtype = '$type' and
  1366.                    magnets.mtid = magnet_types.id order by magnets.val;" );   # PROFILE BLOCK STOP
  1367.  
  1368.     $h->execute;
  1369.     while ( my $row = $h->fetchrow_arrayref ) {
  1370.         push @magnets, [$row->[0], $row->[1]];
  1371.     }
  1372.     $h->finish;
  1373.  
  1374.     foreach my $m (@magnets) {
  1375.         my ( $magnet, $id ) = @{$m};
  1376.         $magnet = lc($magnet);
  1377.  
  1378.         for my $i (0..(length($match)-length($magnet))) {
  1379.             if ( substr( $match, $i, length($magnet)) eq $magnet ) {
  1380.                 $self->{magnet_used__}   = 1;
  1381.                 $self->{magnet_detail__} = $id;
  1382.  
  1383.                 return 1;
  1384.             }
  1385.         }
  1386.     }
  1387.  
  1388.     return 0;
  1389. }
  1390.  
  1391. #----------------------------------------------------------------------------
  1392. #
  1393. # magnet_match__
  1394. #
  1395. # Helper the determines if a specific string matches a certain magnet
  1396. # type in a bucket
  1397. #
  1398. # $session         Valid session from get_session_key
  1399. # $match           The string to match
  1400. # $bucket          The bucket to check
  1401. # $type            The magnet type to check
  1402. #
  1403. #----------------------------------------------------------------------------
  1404. sub magnet_match__
  1405. {
  1406.     my ( $self, $session, $match, $bucket, $type ) = @_;
  1407.  
  1408.     return $self->magnet_match_helper__( $session, $match, $bucket, $type );
  1409. }
  1410.  
  1411. #----------------------------------------------------------------------------
  1412. #
  1413. # write_line__
  1414. #
  1415. # Writes a line to a file and parses it unless the classification is
  1416. # already known
  1417. #
  1418. # $file         File handle for file to write line to
  1419. # $line         The line to write
  1420. # $class        (optional) The current classification
  1421. #
  1422. #----------------------------------------------------------------------------
  1423. sub write_line__
  1424. {
  1425.     my ( $self, $file, $line, $class ) = @_;
  1426.  
  1427.     print $file $line if defined( $file );
  1428.  
  1429.     if ( $class eq '' ) {
  1430.         $self->{parser__}->parse_line( $line );
  1431.     }
  1432. }
  1433.  
  1434. #----------------------------------------------------------------------------
  1435. #
  1436. # add_words_to_bucket__
  1437. #
  1438. # Takes words previously parsed by the mail parser and adds/subtracts
  1439. # them to/from a bucket, this is a helper used by
  1440. # add_messages_to_bucket, remove_message_from_bucket
  1441. #
  1442. # $session        Valid session from get_session_key
  1443. # $bucket         Bucket to add to
  1444. # $subtract       Set to -1 means subtract the words, set to 1 means add
  1445. #
  1446. #----------------------------------------------------------------------------
  1447. sub add_words_to_bucket__
  1448. {
  1449.     my ( $self, $session, $bucket, $subtract ) = @_;
  1450.  
  1451.     my $userid = $self->valid_session_key__( $session );
  1452.     return undef if ( !defined( $userid ) );
  1453.  
  1454.     # Map the list of words to a list of counts currently in the database
  1455.     # then update those counts and write them back to the database.
  1456.  
  1457.     my $words;
  1458.     $words = join( ',', map( $self->{db__}->quote( $_ ), (sort keys %{$self->{parser__}{words__}}) ) );
  1459.     $self->{get_wordids__} = $self->{db__}->prepare(        # PROFILE BLOCK START
  1460.              "select id, word
  1461.                   from words
  1462.                   where word in ( $words );" );             # PROFILE BLOCK STOP
  1463.     $self->{get_wordids__}->execute;
  1464.  
  1465.     my @id_list;
  1466.     my %wordmap;
  1467.  
  1468.     while ( my $row = $self->{get_wordids__}->fetchrow_arrayref ) {
  1469.         push @id_list, ($row->[0]);
  1470.         $wordmap{$row->[1]} = $row->[0];
  1471.     }
  1472.  
  1473.     $self->{get_wordids__}->finish;
  1474.  
  1475.     my $ids = join( ',', @id_list );
  1476.  
  1477.     $self->{db_getwords__} = $self->{db__}->prepare(                                         # PROFILE BLOCK START
  1478.              "select matrix.times, matrix.wordid
  1479.                   from matrix
  1480.                   where matrix.wordid in ( $ids )
  1481.                     and matrix.bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};" );  # PROFILE BLOCK STOP
  1482.  
  1483.     $self->{db_getwords__}->execute;
  1484.  
  1485.     my %counts;
  1486.  
  1487.     while ( my $row = $self->{db_getwords__}->fetchrow_arrayref ) {
  1488.         $counts{$row->[1]} = $row->[0];
  1489.     }
  1490.  
  1491.     $self->{db_getwords__}->finish;
  1492.  
  1493.     $self->{db__}->begin_work;
  1494.     foreach my $word (keys %{$self->{parser__}->{words__}}) {
  1495.  
  1496.         # If there's already a count then it means that the word is
  1497.         # already in the database and we have its id in
  1498.         # $wordmap{$word} so for speed we execute the
  1499.         # db_put_word_count__ query here rather than going through
  1500.         # set_value_ which would need to look up the wordid again
  1501.  
  1502.         if ( defined( $wordmap{$word} ) && defined( $counts{$wordmap{$word}} ) ) {
  1503.             $self->{db_put_word_count__}->execute( $self->{db_bucketid__}{$userid}{$bucket}{id},               # PROFILE BLOCK START
  1504.                 $wordmap{$word}, $counts{$wordmap{$word}} + $subtract * $self->{parser__}->{words__}{$word} ); # PROFILE BLOCK STOP
  1505.         } else {
  1506.  
  1507.             # If the word is not in the database and we are trying to
  1508.             # subtract then we do nothing because negative values are
  1509.             # meaningless
  1510.  
  1511.             if ( $subtract == 1 ) {
  1512.                 $self->db_put_word_count__( $session, $bucket, $word, $self->{parser__}->{words__}{$word} );
  1513.             }
  1514.         }
  1515.     }
  1516.  
  1517.     # If we were doing a subtract operation it's possible that some of
  1518.     # the words in the bucket now have a zero count and should be
  1519.     # removed
  1520.  
  1521.     if ( $subtract == -1 ) {
  1522.         $self->{db_delete_zero_words__}->execute( $self->{db_bucketid__}{$userid}{$bucket}{id} );
  1523.     }
  1524.  
  1525.     $self->{db__}->commit;
  1526. }
  1527.  
  1528. #----------------------------------------------------------------------------
  1529. #
  1530. # echo_to_dot_
  1531. #
  1532. # $mail The stream (created with IO::) to send the message to (the
  1533. # remote mail server)
  1534. # $client (optional) The local mail client (created with IO::) that
  1535. # needs the response
  1536. # $file (optional) A file to print the response to, caller specifies
  1537. # open style
  1538. # $before (optional) String to send to client before the dot is sent
  1539. #
  1540. # echo all information from the $mail server until a single line with
  1541. # a . is seen
  1542. #
  1543. # NOTE Also echoes the line with . to $client but not to $file
  1544. #
  1545. # Returns 1 if there was a . or 0 if reached EOF before we hit the .
  1546. #
  1547. #----------------------------------------------------------------------------
  1548. sub echo_to_dot_
  1549. {
  1550.     my ( $self, $mail, $client, $file, $before ) = @_;
  1551.  
  1552.     my $hit_dot = 0;
  1553.  
  1554.     my $isopen = open FILE, "$file" if ( defined( $file ) );
  1555.     binmode FILE if ($isopen);
  1556.  
  1557.     while ( my $line = $self->slurp_( $mail ) ) {
  1558.  
  1559.         # Check for an abort
  1560.  
  1561.         last if ( $self->{alive_} == 0 );
  1562.  
  1563.         # The termination has to be a single line with exactly a dot
  1564.         # on it and nothing else other than line termination
  1565.         # characters.  This is vital so that we do not mistake a line
  1566.         # beginning with . as the end of the block
  1567.  
  1568.         if ( $line =~ /^\.(\r\n|\r|\n)$/ ) {
  1569.             $hit_dot = 1;
  1570.  
  1571.             if ( defined( $before ) && ( $before ne '' ) ) {
  1572.                 print $client $before if ( defined( $client ) );
  1573.                 print FILE    $before if ( defined( $isopen ) );
  1574.             }
  1575.  
  1576.             # Note that there is no print FILE here.  This is correct
  1577.             # because we do no want the network terminator . to appear
  1578.             # in the file version of any message
  1579.  
  1580.             print $client $line if ( defined( $client ) );
  1581.             last;
  1582.         }
  1583.  
  1584.         print $client $line if ( defined( $client ) );
  1585.         print FILE    $line if ( defined( $isopen ) );
  1586.  
  1587.     }
  1588.  
  1589.     close FILE if ( $isopen );
  1590.  
  1591.     return $hit_dot;
  1592. }
  1593.  
  1594. #----------------------------------------------------------------------------
  1595. #
  1596. # substr_euc__
  1597. #
  1598. # "substr" function which supports EUC Japanese charset
  1599. #
  1600. # $pos      Start position
  1601. # $len      Word length
  1602. #
  1603. #----------------------------------------------------------------------------
  1604. sub substr_euc__
  1605. {
  1606.     my ( $str, $pos, $len ) = @_;
  1607.     my $result_str;
  1608.     my $char;
  1609.     my $count = 0;
  1610.     if ( !$pos ) {
  1611.         $pos = 0;
  1612.     }
  1613.     if ( !$len ) {
  1614.         $len = length( $str );
  1615.     }
  1616.  
  1617.     for ( $pos = 0; $count < $len; $pos++ ) {
  1618.         $char = substr( $str, $pos, 1 );
  1619.         if ( $char =~ /[\x80-\xff]/ ) {
  1620.             $char = substr( $str, $pos++, 2 );
  1621.         }
  1622.         $result_str .= $char;
  1623.         $count++;
  1624.     }
  1625.  
  1626.     return $result_str;
  1627. }
  1628.  
  1629. #----------------------------------------------------------------------------
  1630. #
  1631. # generate_unique_session_key__
  1632. #
  1633. # Returns a unique string based session key that can be used as a key
  1634. # in the api_sessions__
  1635. #
  1636. #----------------------------------------------------------------------------
  1637. sub generate_unique_session_key__
  1638. {
  1639.     my ( $self ) = @_;
  1640.  
  1641.     my @chars = ( 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',   # PROFILE BLOCK START
  1642.                   'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'U', 'V', 'W', 'X', 'Y',
  1643.                   'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A' ); # PROFILE BLOCK STOP
  1644.  
  1645.     my $session;
  1646.  
  1647.     do {
  1648.         $session = '';
  1649.         my $length = int( 16 + rand(4) );
  1650.  
  1651.         for my $i (0 .. $length) {
  1652.             my $random = $chars[int( rand(36) )];
  1653.  
  1654.             # Just to add spice to things we sometimes lowercase the value
  1655.  
  1656.             if ( rand(1) < rand(1) ) {
  1657.                 $random = lc($random);
  1658.             }
  1659.  
  1660.             $session .= $random;
  1661.         }
  1662.     } while ( defined( $self->{api_sessions__}{$session} ) );
  1663.  
  1664.     return $session;
  1665. }
  1666.  
  1667. #----------------------------------------------------------------------------
  1668. #
  1669. # release_session_key_private__
  1670. #
  1671. # $session        A session key previously returned by get_session_key
  1672. #
  1673. # Releases and invalidates the session key. Worker function that does the work
  1674. # of release_session_key. 
  1675. #                   ****DO NOT CALL DIRECTLY****
  1676. # unless you want your session key released immediately, possibly preventing
  1677. # asynchronous tasks from completing
  1678. #
  1679. #----------------------------------------------------------------------------
  1680. sub release_session_key_private__
  1681. {
  1682.     my ( $self, $session ) = @_;
  1683.     
  1684.     if ( defined( $self->{api_sessions__}{$session} ) ) {
  1685.         $self->log_( 1, "release_session_key releasing key $session for user $self->{api_sessions__}{$session}" );
  1686.         delete $self->{api_sessions__}{$session};
  1687.     }
  1688. }
  1689.  
  1690. #----------------------------------------------------------------------------
  1691. #
  1692. # valid_session_key__
  1693. #
  1694. # $session                Session key returned by call to get_session_key
  1695. #
  1696. # Returns undef is the session key is not valid, or returns the user
  1697. # ID associated with the session key which can be used in database
  1698. # accesses
  1699. #
  1700. #----------------------------------------------------------------------------
  1701. sub valid_session_key__
  1702. {
  1703.     my ( $self, $session ) = @_;
  1704.  
  1705.     # This provides protection against someone using the XML-RPC
  1706.     # interface and calling this API directly to fish for session
  1707.     # keys, this must be called from within this module
  1708.  
  1709.     return undef if ( caller ne 'Classifier::Bayes' );
  1710.  
  1711.     # If the session key is invalid then wait 1 second.  This is done
  1712.     # to prevent people from calling a POPFile API such as
  1713.     # get_bucket_count with random session keys fishing for a valid
  1714.     # key.  The XML-RPC API is single threaded and hence this will
  1715.     # delay all use of that API by one second.  Of course in normal
  1716.     # use when the user knows the username/password or session key
  1717.     # then there is no delay
  1718.  
  1719.     if ( !defined( $self->{api_sessions__}{$session} ) ) {
  1720.         my ( $package, $filename, $line, $subroutine ) = caller;
  1721.         $self->log_( 0, "Invalid session key $session provided in $package @ $line" );
  1722.         select( undef, undef, undef, 1 );
  1723.     }
  1724.  
  1725.     return $self->{api_sessions__}{$session};
  1726. }
  1727.  
  1728. #----------------------------------------------------------------------------
  1729. #----------------------------------------------------------------------------
  1730. # _____   _____   _____  _______ _____        _______   _______  _____  _____
  1731. #|_____] |     | |_____] |______   |   |      |______   |_____| |_____]   |
  1732. #|       |_____| |       |       __|__ |_____ |______   |     | |       __|__
  1733. #
  1734. # The method below are public and may be accessed by other modules.
  1735. # All of them may be accessed remotely through the XMLRPC.pm module
  1736. # using the XML-RPC protocol
  1737. #
  1738. # Note that every API function expects to be passed a $session which
  1739. # is obtained by first calling get_session_key with a valid username
  1740. # and password.  Once done call the method release_session_key.
  1741. #
  1742. # See POPFile::API for more details
  1743. #
  1744. #----------------------------------------------------------------------------
  1745. #----------------------------------------------------------------------------
  1746.  
  1747. #----------------------------------------------------------------------------
  1748. #
  1749. # get_session_key
  1750. #
  1751. # $user           The name of an existing user
  1752. # $pwd            The user's password
  1753. #
  1754. # Returns a string based session key if the username and password
  1755. # match, or undef if not
  1756. #
  1757. #----------------------------------------------------------------------------
  1758. sub get_session_key
  1759. {
  1760.     my ( $self, $user, $pwd ) = @_;
  1761.  
  1762.     # The password is stored in the database as an MD5 hash of the
  1763.     # username and password concatenated and separated by the string
  1764.     # __popfile__, so compute the hash here
  1765.  
  1766.     my $hash = md5_hex( $user . '__popfile__' . $pwd );
  1767.  
  1768.     $self->{db_get_userid__}->execute( $user, $hash );
  1769.     my $result = $self->{db_get_userid__}->fetchrow_arrayref;
  1770.     if ( !defined( $result ) ) {
  1771.  
  1772.         # The delay of one second here is to prevent people from trying out
  1773.         # username/password combinations at high speed to determine the
  1774.         # credentials of a valid user
  1775.  
  1776.         $self->log_( 0, "Attempt to login with incorrect credentials for user $user" );
  1777.         select( undef, undef, undef, 1 );
  1778.         return undef;
  1779.     }
  1780.  
  1781.     my $session = $self->generate_unique_session_key__();
  1782.  
  1783.     $self->{api_sessions__}{$session} = $result->[0];
  1784.  
  1785.     $self->db_update_cache__( $session );
  1786.  
  1787.     $self->log_( 1, "get_session_key returning key $session for user $self->{api_sessions__}{$session}" );
  1788.  
  1789.     return $session;
  1790. }
  1791.  
  1792. #----------------------------------------------------------------------------
  1793. #
  1794. # release_session_key
  1795. #
  1796. # $session        A session key previously returned by get_session_key
  1797. #
  1798. # Releases and invalidates the session key
  1799. #
  1800. #----------------------------------------------------------------------------
  1801. sub release_session_key
  1802. {
  1803.     my ( $self, $session ) = @_;
  1804.     
  1805.     $self->mq_post_( "RELSE", $session );
  1806. }
  1807.  
  1808.  
  1809. #----------------------------------------------------------------------------
  1810. #
  1811. # get_top_bucket__
  1812. #
  1813. # Helper function used by classify to get the bucket with the highest
  1814. # score from data stored in a matrix of information (see definition of
  1815. # %matrix in classify for details) and a list of potential buckets
  1816. #
  1817. # $userid         User ID for database access
  1818. # $id             ID of a word in $matrix
  1819. # $matrix         Reference to the %matrix hash in classify
  1820. # $buckets        Reference to a list of buckets
  1821. #
  1822. # Returns the bucket in $buckets with the highest score
  1823. #
  1824. #----------------------------------------------------------------------------
  1825. sub get_top_bucket__
  1826. {
  1827.     my ( $self, $userid, $id, $matrix, $buckets ) = @_;
  1828.  
  1829.     my $best_probability = 0;
  1830.     my $top_bucket       = 'unclassified';
  1831.  
  1832.     for my $bucket (@$buckets) {
  1833.         my $probability = 0;
  1834.         if ( defined($$matrix{$id}{$bucket}) && ( $$matrix{$id}{$bucket} > 0 ) ) {
  1835.             $probability = $$matrix{$id}{$bucket} / $self->{db_bucketcount__}{$userid}{$bucket};
  1836.         }
  1837.  
  1838.         if ( $probability > $best_probability ) {
  1839.             $best_probability = $probability;
  1840.             $top_bucket       = $bucket;
  1841.         }
  1842.     }
  1843.  
  1844.     return $top_bucket;
  1845. }
  1846.  
  1847. #----------------------------------------------------------------------------
  1848. #
  1849. # classify
  1850. #
  1851. # $session   A valid session key returned by a call to get_session_key
  1852. # $file The name of the file containing the text to classify (or undef
  1853. # to use the data already in the parser)
  1854. # $templ     Reference to the UI template used for word score display
  1855. # $matrix (optional) Reference to a hash that will be filled with the
  1856. # word matrix used in classification
  1857. # $idmap (optional) Reference to a hash that will map word ids in the
  1858. # $matrix to actual words
  1859. #
  1860. # Splits the mail message into valid words, then runs the Bayes
  1861. # algorithm to figure out which bucket it belongs in.  Returns the
  1862. # bucket name
  1863. #
  1864. #----------------------------------------------------------------------------
  1865. sub classify
  1866. {
  1867.     my ( $self, $session, $file, $templ, $matrix, $idmap ) = @_;
  1868.     my $msg_total = 0;
  1869.  
  1870.     my $userid = $self->valid_session_key__( $session );
  1871.     return undef if ( !defined( $userid ) );
  1872.  
  1873.     $self->{unclassified__} = log( $self->config_( 'unclassified_weight' ) );
  1874.  
  1875.     $self->{magnet_used__}   = 0;
  1876.     $self->{magnet_detail__} = 0;
  1877.  
  1878.     if ( defined( $file ) ) {
  1879.         $self->{parser__}->parse_file( $file,                                           # PROFILE BLOCK START
  1880.                                        $self->global_config_( 'message_cutoff'   ) );   # PROFILE BLOCK STOP
  1881.     }
  1882.  
  1883.     # Check to see if this email should be classified based on a magnet
  1884.     # Get the list of buckets
  1885.  
  1886.     my @buckets = $self->get_buckets( $session );
  1887.  
  1888.     for my $bucket ($self->get_buckets_with_magnets( $session ))  {
  1889.         for my $type ($self->get_magnet_types_in_bucket( $session, $bucket )) {
  1890.             if ( $self->magnet_match__( $session, $self->{parser__}->get_header($type), $bucket, $type ) ) {
  1891.                 return $bucket;
  1892.             }
  1893.         }
  1894.     }
  1895.  
  1896.     # If the user has not defined any buckets then we escape here
  1897.     # return unclassified
  1898.  
  1899.     return "unclassified" if ( $#buckets == -1 );
  1900.  
  1901.     # The score hash will contain the likelihood that the given
  1902.     # message is in each bucket, the buckets are the keys for score
  1903.  
  1904.     # Set up the initial score as P(bucket)
  1905.  
  1906.     my %score;
  1907.     my %matchcount;
  1908.  
  1909.     # Build up a list of the buckets that are OK to use for
  1910.     # classification (i.e.  that have at least one word in them).
  1911.  
  1912.     my @ok_buckets;
  1913.  
  1914.     for my $bucket (@buckets) {
  1915.         if ( $self->{bucket_start__}{$userid}{$bucket} != 0 ) {
  1916.             $score{$bucket} = $self->{bucket_start__}{$userid}{$bucket};
  1917.             $matchcount{$bucket} = 0;
  1918.             push @ok_buckets, ( $bucket );
  1919.         }
  1920.     }
  1921.  
  1922.     @buckets = @ok_buckets;
  1923.  
  1924.     # For each word go through the buckets and calculate
  1925.     # P(word|bucket) and then calculate P(word|bucket) ^ word count
  1926.     # and multiply to the score
  1927.  
  1928.     my $word_count = 0;
  1929.  
  1930.     # The correction value is used to generate score displays variable
  1931.     # which are consistent with the word scores shown by the GUI's
  1932.     # word lookup feature.  It is computed to make the contribution of
  1933.     # a word which is unrepresented in a bucket zero.  This correction
  1934.     # affects only the values displayed in the display; it has no
  1935.     # effect on the classification process.
  1936.  
  1937.     my $correction = 0;
  1938.  
  1939.     # Classification against the database works in a sequence of steps
  1940.     # to get the fastest time possible.  The steps are as follows:
  1941.     #
  1942.     # 1. Convert the list of words returned by the parser into a list
  1943.     #    of unique word ids that can be used in the database.  This
  1944.     #    requires a select against the database to get the word ids
  1945.     #    (and associated words) which is then converted into two
  1946.     #    things: @id_list which is just the sorted list of word ids
  1947.     #    and %idmap which maps a word to its id.
  1948.     #
  1949.     # 2. Then run a second select that get the triplet (count, id,
  1950.     #    bucket) for each word id and each bucket.  The triplet
  1951.     #    contains the word count from the database for each bucket and
  1952.     #    each id, where there is an entry. That data gets loaded into
  1953.     #    the sparse matrix %matrix.
  1954.     #
  1955.     # 3. Do the normal classification loop as before running against
  1956.     # the @id_list for the words and for each bucket.  If there's an
  1957.     # entry in %matrix for the id/bucket combination then calculate
  1958.     # the probability, otherwise use the not_likely probability.
  1959.     #
  1960.     # NOTE.  Since there is a single not_likely probability we do not
  1961.     # worry about the fact that the select in 1 might return a shorter
  1962.     # list of words than was found in the message (because some words
  1963.     # are not in the database) since the missing words will be the
  1964.     # same for all buckets and hence constitute a fixed scaling factor
  1965.     # on all the buckets which is irrelevant in deciding which the
  1966.     # winning bucket is.
  1967.  
  1968.     my $words;
  1969.     $words = join( ',', map( $self->{db__}->quote( $_ ), (sort keys %{$self->{parser__}{words__}}) ) );
  1970.     $self->{get_wordids__} = $self->{db__}->prepare(  # PROFILE BLOCK START
  1971.              "select id, word
  1972.                   from words
  1973.                   where word in ( $words )
  1974.                   order by id;" );                    # PROFILE BLOCK STOP
  1975.     $self->{get_wordids__}->execute;
  1976.  
  1977.     my @id_list;
  1978.     my %temp_idmap;
  1979.  
  1980.     if ( !defined( $idmap ) ) {
  1981.         $idmap = \%temp_idmap;
  1982.     }
  1983.  
  1984.     while ( my $row = $self->{get_wordids__}->fetchrow_arrayref ) {
  1985.         push @id_list, ($row->[0]);
  1986.         $$idmap{$row->[0]} = $row->[1];
  1987.     }
  1988.  
  1989.     $self->{get_wordids__}->finish;
  1990.  
  1991.     my $ids = join( ',', @id_list );
  1992.  
  1993.     $self->{db_classify__} = $self->{db__}->prepare(            # PROFILE BLOCK START
  1994.              "select matrix.times, matrix.wordid, buckets.name
  1995.                   from matrix, buckets
  1996.                   where matrix.wordid in ( $ids )
  1997.                     and matrix.bucketid = buckets.id
  1998.                     and buckets.userid = $userid;" );           # PROFILE BLOCK STOP
  1999.  
  2000.     $self->{db_classify__}->execute;
  2001.  
  2002.     # %matrix maps wordids and bucket names to counts
  2003.     # $matrix{$wordid}{$bucket} == $count
  2004.  
  2005.     my %temp_matrix;
  2006.  
  2007.     if ( !defined( $matrix ) ) {
  2008.         $matrix = \%temp_matrix;
  2009.     }
  2010.  
  2011.     while ( my $row = $self->{db_classify__}->fetchrow_arrayref ) {
  2012.         $$matrix{$row->[1]}{$row->[2]} = $row->[0];
  2013.     }
  2014.  
  2015.     $self->{db_classify__}->finish;
  2016.  
  2017.     foreach my $id (@id_list) {
  2018.         $word_count += 2;
  2019.         my $wmax = -10000;
  2020.  
  2021.         foreach my $bucket (@buckets) {
  2022.             my $probability = 0;
  2023.  
  2024.             if ( defined($$matrix{$id}{$bucket}) && ( $$matrix{$id}{$bucket} > 0 ) ) {
  2025.                 $probability = log( $$matrix{$id}{$bucket} / $self->{db_bucketcount__}{$userid}{$bucket} );
  2026.             }
  2027.  
  2028.             $matchcount{$bucket} += $self->{parser__}{words__}{$$idmap{$id}} if ($probability != 0);
  2029.             $probability = $self->{not_likely__}{$userid} if ( $probability == 0 );
  2030.             $wmax = $probability if ( $wmax < $probability );
  2031.             $score{$bucket} += ( $probability * $self->{parser__}{words__}{$$idmap{$id}} );
  2032.         }
  2033.  
  2034.         if ($wmax > $self->{not_likely__}{$userid}) {
  2035.             $correction += $self->{not_likely__}{$userid} * $self->{parser__}{words__}{$$idmap{$id}};
  2036.         } else {
  2037.             $correction += $wmax * $self->{parser__}{words__}{$$idmap{$id}};
  2038.         }
  2039.     }
  2040.  
  2041.     # Now sort the scores to find the highest and return that bucket
  2042.     # as the classification
  2043.  
  2044.     my @ranking = sort {$score{$b} <=> $score{$a}} keys %score;
  2045.  
  2046.     my %raw_score;
  2047.     my $base_score = $score{$ranking[0]};
  2048.     my $total = 0;
  2049.  
  2050.     # If the first and second bucket are too close in their
  2051.     # probabilities, call the message unclassified.  Also if there are
  2052.     # fewer than 2 buckets.
  2053.  
  2054.     my $class = 'unclassified';
  2055.  
  2056.     if ( @buckets > 1 && $score{$ranking[0]} > ( $score{$ranking[1]} + $self->{unclassified__} ) ) {
  2057.         $class = $ranking[0];
  2058.     }
  2059.  
  2060.     # Compute the total of all the scores to generate the normalized
  2061.     # scores and probability estimate.  $total is always 1 after the
  2062.     # first loop iteration, so any additional term less than 2 ** -54
  2063.     # is insignificant, and need not be computed.
  2064.  
  2065.     my $ln2p_54 = -54 * log(2);
  2066.  
  2067.     foreach my $b (@ranking) {
  2068.         $raw_score{$b} = $score{$b};
  2069.         $score{$b} -= $base_score;
  2070.  
  2071.         $total += exp($score{$b}) if ($score{$b} > $ln2p_54 );
  2072.     }
  2073.  
  2074.     if ($self->{wordscores__} && defined($templ) ) {
  2075.         my %qm = %{$self->{parser__}->quickmagnets()};
  2076.         my $mlen = scalar(keys %{$self->{parser__}->quickmagnets()});
  2077.  
  2078.         if ( $mlen >= 0 ) {
  2079.             $templ->param( 'View_QuickMagnets_If' => 1 );
  2080.             $templ->param( 'View_QuickMagnets_Count' => ($mlen + 1) );
  2081.             my @buckets = $self->get_buckets( $session );
  2082.             my $i = 0;
  2083.             my %types = $self->get_magnet_types( $session );
  2084.  
  2085.             my @bucket_data;
  2086.             foreach my $bucket (@buckets) {
  2087.                 my %row_data;
  2088.                 $row_data{View_QuickMagnets_Bucket} = $bucket;
  2089.                 $row_data{View_QuickMagnets_Bucket_Color} = $self->get_bucket_color( $session, $bucket );
  2090.                 push ( @bucket_data, \%row_data );
  2091.             }
  2092.  
  2093.             my @qm_data;
  2094.             foreach my $type (sort keys %types) {
  2095.                 my %row_data;
  2096.  
  2097.                 if (defined $qm{$type}) {
  2098.                     $i++;
  2099.  
  2100.                     $row_data{View_QuickMagnets_Type} = $type;
  2101.                     $row_data{View_QuickMagnets_I} = $i;
  2102.                     $row_data{View_QuickMagnets_Loop_Buckets} = \@bucket_data;
  2103.  
  2104.                     my @magnet_data;
  2105.                     foreach my $magnet ( @{$qm{$type}} ) {
  2106.                         my %row_magnet;
  2107.                         $row_magnet{View_QuickMagnets_Magnet} = $magnet;
  2108.                         push ( @magnet_data, \%row_magnet );
  2109.                     }
  2110.                     $row_data{View_QuickMagnets_Loop_Magnets} = \@magnet_data;
  2111.  
  2112.                     push ( @qm_data, \%row_data );
  2113.                 }
  2114.             }
  2115.             $templ->param( 'View_QuickMagnets_Loop' => \@qm_data );
  2116.         }
  2117.  
  2118.         $templ->param( 'View_Score_If_Score' => $self->{wmformat__} eq 'score' );
  2119.         my $log10 = log(10.0);
  2120.  
  2121.         my @score_data;
  2122.         foreach my $b (@ranking) {
  2123.              my %row_data;
  2124.              my $prob = exp($score{$b})/$total;
  2125.              my $probstr;
  2126.              my $rawstr;
  2127.  
  2128.              # If the computed probability would display as 1, display
  2129.              # it as .999999 instead.  We don't want to give the
  2130.              # impression that POPFile is ever completely sure of its
  2131.              # classification.
  2132.  
  2133.              if ($prob >= .999999) {
  2134.                  $probstr = sprintf("%12.6f", 0.999999);
  2135.              } else {
  2136.                  if ($prob >= 0.1 || $prob == 0.0) {
  2137.                      $probstr = sprintf("%12.6f", $prob);
  2138.                  } else {
  2139.                     $probstr = sprintf("%17.6e", $prob);
  2140.                  }
  2141.              }
  2142.  
  2143.              my $color = $self->get_bucket_color( $session, $b );
  2144.  
  2145.              $row_data{View_Score_Bucket} = $b;
  2146.              $row_data{View_Score_Bucket_Color} = $color;
  2147.              $row_data{View_Score_MatchCount} = $matchcount{$b};
  2148.              $row_data{View_Score_ProbStr} = $probstr;
  2149.  
  2150.              if ($self->{wmformat__} eq 'score') {
  2151.                  $row_data{View_Score_If_Score} = 1;
  2152.                  $rawstr = sprintf("%12.6f", ($raw_score{$b} - $correction)/$log10);
  2153.                  $row_data{View_Score_RawStr} = $rawstr;
  2154.              }
  2155.              push ( @score_data, \%row_data );
  2156.         }
  2157.         $templ->param( 'View_Score_Loop_Scores' => \@score_data );
  2158.  
  2159.         if ( $self->{wmformat__} ne '' ) {
  2160.             $templ->param( 'View_Score_If_Table' => 1 );
  2161.  
  2162.             my @header_data;
  2163.             foreach my $ix (0..($#buckets > 7? 7: $#buckets)) {
  2164.                 my %row_data;
  2165.                 my $bucket = $ranking[$ix];
  2166.                 my $bucketcolor  = $self->get_bucket_color( $session, $bucket );
  2167.                 $row_data{View_Score_Bucket} = $bucket;
  2168.                 $row_data{View_Score_Bucket_Color} = $bucketcolor;
  2169.                 push ( @header_data, \%row_data );
  2170.             }
  2171.             $templ->param( 'View_Score_Loop_Bucket_Header' => \@header_data );
  2172.  
  2173.             my %wordprobs;
  2174.  
  2175.             # If the word matrix is supposed to show probabilities,
  2176.             # compute them, saving the results in %wordprobs.
  2177.  
  2178.             if ( $self->{wmformat__} eq 'prob') {
  2179.                 foreach my $id (@id_list) {
  2180.                     my $sumfreq = 0;
  2181.                     my %wval;
  2182.                     foreach my $bucket (@ranking) {
  2183.                         $wval{$bucket} = $$matrix{$id}{$bucket} || 0;
  2184.                         $sumfreq += $wval{$bucket};
  2185.                     }
  2186.  
  2187.                     # If $sumfreq is still zero then this word didn't
  2188.                     # appear in any buckets so we shouldn't create
  2189.                     # wordprobs entries for it
  2190.  
  2191.                     if ( $sumfreq != 0 ) {
  2192.                         foreach my $bucket (@ranking) {
  2193.                             $wordprobs{$bucket,$id} = $wval{$bucket} / $sumfreq;
  2194.                         }
  2195.                     }
  2196.                 }
  2197.             }
  2198.  
  2199.             my @ranked_ids;
  2200.             if ($self->{wmformat__} eq 'prob') {
  2201.                 @ranked_ids = sort {($wordprobs{$ranking[0],$b}||0) <=> ($wordprobs{$ranking[0],$a}||0)} @id_list;
  2202.             } else {
  2203.                 @ranked_ids = sort {($$matrix{$b}{$ranking[0]}||0) <=> ($$matrix{$a}{$ranking[0]}||0)} @id_list;
  2204.             }
  2205.  
  2206.             my @word_data;
  2207.             my %chart;
  2208.             foreach my $id (@ranked_ids) {
  2209.                 my %row_data;
  2210.                 my $known = 0;
  2211.  
  2212.                 foreach my $bucket (@ranking) {
  2213.                     if ( defined( $$matrix{$id}{$bucket} ) ) {
  2214.                         $known = 1;
  2215.                         last;
  2216.                     }
  2217.                 }
  2218.  
  2219.                 if ( $known == 1 ) {
  2220.                     my $wordcolor = $self->get_bucket_color( $session, $self->get_top_bucket__( $userid, $id, $matrix, \@ranking ) );
  2221.                     my $count = $self->{parser__}->{words__}{$$idmap{$id}};
  2222.  
  2223.                     $row_data{View_Score_Word} = $$idmap{$id};
  2224.                     $row_data{View_Score_Word_Color} = $wordcolor;
  2225.                     $row_data{View_Score_Word_Count} = $count;
  2226.  
  2227.                     my $base_probability = 0;
  2228.                     if ( defined($$matrix{$id}{$ranking[0]}) && ( $$matrix{$id}{$ranking[0]} > 0 ) ) {
  2229.                         $base_probability = log( $$matrix{$id}{$ranking[0]} / $self->{db_bucketcount__}{$userid}{$ranking[0]} );
  2230.                     }
  2231.  
  2232.                     my @per_bucket;
  2233.                     my @score;
  2234.                     foreach my $ix (0..($#buckets > 7? 7: $#buckets)) {
  2235.                         my %bucket_row;
  2236.                         my $bucket = $ranking[$ix];
  2237.                         my $probability = 0;
  2238.                         if ( defined($$matrix{$id}{$bucket}) && ( $$matrix{$id}{$bucket} > 0 ) ) {
  2239.                             $probability = log( $$matrix{$id}{$bucket} / $self->{db_bucketcount__}{$userid}{$bucket} );
  2240.                         }
  2241.                         my $color = 'black';
  2242.  
  2243.                         if ( $probability >= $base_probability || $base_probability == 0 ) {
  2244.                             $color = $self->get_bucket_color( $session, $bucket );
  2245.                         }
  2246.  
  2247.                         $bucket_row{View_Score_If_Probability} = ( $probability != 0 );
  2248.                         $bucket_row{View_Score_Word_Color} = $color;
  2249.                         if ( $probability != 0 ) {
  2250.                             my $wordprobstr;
  2251.                             if ($self->{wmformat__} eq 'score') {
  2252.                                 $wordprobstr  = sprintf("%12.4f", ($probability - $self->{not_likely__}{$userid})/$log10 );
  2253.                                 push ( @score, $wordprobstr );
  2254.                             } else {
  2255.                                 if ($self->{wmformat__} eq 'prob') {
  2256.                                     $wordprobstr  = sprintf("%12.4f", $wordprobs{$bucket,$id});
  2257.                                 } else {
  2258.                                     $wordprobstr  = sprintf("%13.5f", exp($probability) );
  2259.                                 }
  2260.                             }
  2261.                             $bucket_row{View_Score_Probability} = $wordprobstr;
  2262.                         }
  2263.                         else {
  2264.                             # Scores eq 0 must also be remembered.
  2265.                             push @score, 0;
  2266.                         }
  2267.                         push ( @per_bucket, \%bucket_row );
  2268.                     }
  2269.                     $row_data{View_Score_Loop_Per_Bucket} = \@per_bucket;
  2270.  
  2271.                     # If we are doing the word scores then we build up
  2272.                     # a hash that maps the name of a word to a value
  2273.                     # which is the difference between the word scores
  2274.                     # for the top two buckets.  We later use this to
  2275.                     # draw a chart
  2276.  
  2277.                     if ( $self->{wmformat__} eq 'score' ) {
  2278.                         $chart{$$idmap{$id}} = ( $score[0] || 0 ) - ( $score[1] || 0 );
  2279.                     }
  2280.  
  2281.                     push ( @word_data, \%row_data );
  2282.                 }
  2283.             }
  2284.             $templ->param( 'View_Score_Loop_Words' => \@word_data );
  2285.  
  2286.             if ( $self->{wmformat__} eq 'score' ) {
  2287.                 # Draw a chart that shows how the decision between the top
  2288.                 # two buckets was made.
  2289.  
  2290.                 my @words = sort { $chart{$b} <=> $chart{$a} } keys %chart;
  2291.  
  2292.                 my @chart_data;
  2293.                 my $max_chart = $chart{$words[0]};
  2294.                 my $min_chart = $chart{$words[$#words]};
  2295.                 my $scale = ( $max_chart > $min_chart ) ? 400 / ( $max_chart - $min_chart ) : 0;
  2296.  
  2297.                 my $color_1 = $self->get_bucket_color( $session, $ranking[0] );
  2298.                 my $color_2 = $self->get_bucket_color( $session, $ranking[1] );
  2299.  
  2300.                 $templ->param( 'Bucket_1' => $ranking[0] );
  2301.                 $templ->param( 'Bucket_2' => $ranking[1] );
  2302.  
  2303.                 $templ->param( 'Color_Bucket_1' => $color_1 );
  2304.                 $templ->param( 'Color_Bucket_2' => $color_2 );
  2305.  
  2306.                 $templ->param( 'Score_Bucket_1' => sprintf("%.3f", ($raw_score{$ranking[0]} - $correction)/$log10) );
  2307.                 $templ->param( 'Score_Bucket_2' => sprintf("%.3f", ($raw_score{$ranking[1]} - $correction)/$log10) );
  2308.  
  2309.                 for ( my $i=0; $i <= $#words; $i++ ) {
  2310.                     my $word_1 = $words[$i];
  2311.                     my $word_2 = $words[$#words - $i];
  2312.  
  2313.                     my $width_1 = int( $chart{$word_1} * $scale + .5 );
  2314.                     my $width_2 = int( $chart{$word_2} * $scale - .5 ) * -1;
  2315.  
  2316.                     last if ( $width_1 <=0 && $width_2 <= 0 );
  2317.                     
  2318.                     my %row_data;
  2319.  
  2320.                     $row_data{View_Chart_Word_1} = $word_1;
  2321.                     if ( $width_1 > 0 ) {
  2322.                         $row_data{View_If_Bar_1} = 1;
  2323.                         $row_data{View_Width_1}  = $width_1;
  2324.                         $row_data{View_Color_1}  = $color_1;
  2325.                         $row_data{Score_Word_1}  = sprintf "%.3f", $chart{$word_1};
  2326.                     }
  2327.                     else {
  2328.                         $row_data{View_If_Bar_1} = 0;
  2329.                     }
  2330.  
  2331.                     $row_data{View_Chart_Word_2} = $word_2;
  2332.                     if ( $width_2 > 0 ) {
  2333.                         $row_data{View_If_Bar_2} = 1;
  2334.                         $row_data{View_Width_2}  = $width_2;
  2335.                         $row_data{View_Color_2}  = $color_2;
  2336.                         $row_data{Score_Word_2}  = sprintf "%.3f", $chart{$word_2};
  2337.                     }
  2338.                     else {
  2339.                         $row_data{View_If_Bar_2} = 0;
  2340.                     }
  2341.  
  2342.                     push ( @chart_data, \%row_data );
  2343.                 }
  2344.                 $templ->param( 'View_Loop_Chart' => \@chart_data );
  2345.                 $templ->param( 'If_chart' => 1 );
  2346.             }
  2347.             else {
  2348.                 $templ->param( 'If_chart' => 0 );
  2349.             }
  2350.         }
  2351.     }
  2352.  
  2353.     return $class;
  2354. }
  2355.  
  2356. #----------------------------------------------------------------------------
  2357. #
  2358. # classify_and_modify
  2359. #
  2360. # This method reads an email terminated by . on a line by itself (or
  2361. # the end of stream) from a handle and creates an entry in the
  2362. # history, outputting the same email on another handle with the
  2363. # appropriate header modifications and insertions
  2364. #
  2365. # $session  - A valid session key returned by a call to get_session_key
  2366. # $mail     - an open stream to read the email from
  2367. # $client   - an open stream to write the modified email to
  2368. # $nosave   - set to 1 indicates that this should not save to history
  2369. # $class    - if we already know the classification
  2370. # $slot     - Must be defined if $class is set
  2371. # $echo     - 1 to echo to the client, 0 to supress, defaults to 1
  2372. # $crlf     - The sequence to use at the end of a line in the output,
  2373. #   normally this is left undefined and this method uses $eol (the
  2374. #   normal network end of line), but if this method is being used with
  2375. #   real files you may wish to pass in \n instead
  2376. #
  2377. # Returns a classification if it worked and the slot ID of the history
  2378. # item related to this classification
  2379. #
  2380. # IMPORTANT NOTE: $mail and $client should be binmode
  2381. #
  2382. #----------------------------------------------------------------------------
  2383. sub classify_and_modify
  2384. {
  2385.     my ( $self, $session, $mail, $client, $nosave, $class, $slot, $echo, $crlf ) = @_;
  2386.  
  2387.     $echo = 1    unless (defined $echo);
  2388.     $crlf = $eol unless (defined $crlf);
  2389.  
  2390.     my $msg_subject;              # The message subject
  2391.     my $msg_head_before = '';     # Store the message headers that
  2392.                                   # come before Subject here
  2393.     my $msg_head_after = '';      # Store the message headers that
  2394.                                   # come after Subject here
  2395.     my $msg_head_q      = '';     # Store questionable header lines here
  2396.     my $msg_body        = '';     # Store the message body here
  2397.     my $in_subject_header = 0;    # 1 if in Subject header
  2398.  
  2399.     # These two variables are used to control the insertion of the
  2400.     # X-POPFile-TimeoutPrevention header when downloading long or slow
  2401.     # emails
  2402.  
  2403.     my $last_timeout   = time;
  2404.     my $timeout_count  = 0;
  2405.  
  2406.     # Indicates whether the first time through the receive loop we got
  2407.     # the full body, this will happen on small emails
  2408.  
  2409.     my $got_full_body  = 0;
  2410.  
  2411.     # The size of the message downloaded so far.
  2412.  
  2413.     my $message_size   = 0;
  2414.  
  2415.     # The classification for this message
  2416.  
  2417.     my $classification = '';
  2418.  
  2419.     # Whether we are currently reading the mail headers or not
  2420.  
  2421.     my $getting_headers = 1;
  2422.  
  2423.     my $msg_file;
  2424.  
  2425.     # If we don't yet know the classification then start the parser
  2426.  
  2427.     $class = '' if ( !defined( $class ) );
  2428.     if ( $class eq '' ) {
  2429.         $self->{parser__}->start_parse();
  2430.         ( $slot, $msg_file ) = $self->{history__}->reserve_slot();
  2431.     } else {
  2432.         $msg_file = $self->{history__}->get_slot_file( $slot );
  2433.     }
  2434.  
  2435.     # We append .TMP to the filename for the MSG file so that if we are in
  2436.     # middle of downloading a message and we refresh the history we do not
  2437.     # get class file errors
  2438.  
  2439.     open MSG, ">$msg_file" unless $nosave;
  2440.  
  2441.     while ( my $line = $self->slurp_( $mail ) ) {
  2442.         my $fileline;
  2443.  
  2444.         # This is done so that we remove the network style end of line
  2445.         # CR LF and allow Perl to decide on the local system EOL which
  2446.         # it will expand out of \n when this gets written to the temp
  2447.         # file
  2448.  
  2449.         $fileline = $line;
  2450.         $fileline =~ s/[\r\n]//g;
  2451.         $fileline .= "\n";
  2452.  
  2453.         # Check for an abort
  2454.  
  2455.         last if ( $self->{alive_} == 0 );
  2456.  
  2457.         # The termination of a message is a line consisting of exactly
  2458.         # .CRLF so we detect that here exactly
  2459.  
  2460.         if ( $line =~ /^\.(\r\n|\r|\n)$/ ) {
  2461.             $got_full_body = 1;
  2462.             last;
  2463.         }
  2464.  
  2465.         if ( $getting_headers )  {
  2466.  
  2467.             # Kill header lines containing only whitespace (Exim does this)
  2468.  
  2469.             next if ( $line =~ /^[ \t]+(\r\n|\r|\n)$/i );
  2470.  
  2471.             if ( !( $line =~ /^(\r\n|\r|\n)$/i ) )  {
  2472.                 $message_size += length $line;
  2473.                 $self->write_line__( $nosave?undef:\*MSG, $fileline, $class );
  2474.  
  2475.                 # If there is no echoing occuring, it doesn't matter
  2476.                 # what we do to these
  2477.  
  2478.                 if ( $echo ) {
  2479.                     if ( $line =~ /^Subject:(.*)/i )  {
  2480.                         $msg_subject = $1;
  2481.                         $msg_subject =~ s/(\012|\015)//g;
  2482.                         $in_subject_header = 1;
  2483.                         next;
  2484.                     } elsif ( $line !~ /^[ \t]/ ) {
  2485.                         $in_subject_header = 0;
  2486.                     }
  2487.  
  2488.                     # Strip out the X-Text-Classification header that
  2489.                     # is in an incoming message
  2490.  
  2491.                     next if ( $line =~ /^X-Text-Classification:/i );
  2492.                     next if ( $line =~ /^X-POPFile-Link:/i );
  2493.  
  2494.                     # Store any lines that appear as though they may
  2495.                     # be non-header content Lines that are headers
  2496.                     # begin with whitespace or Alphanumerics and "-"
  2497.                     # followed by a colon.
  2498.                     #
  2499.                     # This prevents weird things like HTML before the
  2500.                     # headers terminate from causing the XPL and XTC
  2501.                     # headers to be inserted in places some clients
  2502.                     # can't detect
  2503.  
  2504.                     if ( ( $line =~ /^[ \t]/ ) && $in_subject_header ) {
  2505.                         $line =~ s/(\012|\015)//g;
  2506.                         $msg_subject .= $crlf . $line;
  2507.                         next;
  2508.                     }
  2509.  
  2510.                     if ( $line =~ /^([ \t]|([A-Z0-9\-_]+:))/i ) {
  2511.                         if ( !defined($msg_subject) )  {
  2512.                             $msg_head_before .= $msg_head_q . $line;
  2513.                         } else {
  2514.                             $msg_head_after  .= $msg_head_q . $line;
  2515.                         }
  2516.                         $msg_head_q = '';
  2517.                     } else {
  2518.  
  2519.                         # Gather up any header lines that are questionable
  2520.  
  2521.                         $self->log_( 1, "Found odd email header: $line" );
  2522.                         $msg_head_q .= $line;
  2523.                     }
  2524.                 }
  2525.             } else {
  2526.                 $self->write_line__( $nosave?undef:\*MSG, "\n", $class );
  2527.                 $message_size += length $crlf;
  2528.                 $getting_headers = 0;
  2529.             }
  2530.         } else {
  2531.             $message_size += length $line;
  2532.             $msg_body     .= $line;
  2533.             $self->write_line__( $nosave?undef:\*MSG, $fileline, $class );
  2534.         }
  2535.  
  2536.         # Check to see if too much time has passed and we need to keep
  2537.         # the mail client happy
  2538.  
  2539.         if ( time > ( $last_timeout + 2 ) ) {
  2540.             print $client "X-POPFile-TimeoutPrevention: $timeout_count$crlf" if ( $echo );
  2541.             $timeout_count += 1;
  2542.             $last_timeout = time;
  2543.         }
  2544.  
  2545.         last if ( ( $message_size > $self->global_config_( 'message_cutoff' ) ) && ( $getting_headers == 0 ) );
  2546.     }
  2547.  
  2548.     close MSG unless $nosave;
  2549.  
  2550.     # If we don't yet know the classification then stop the parser
  2551.     if ( $class eq '' ) {
  2552.         $self->{parser__}->stop_parse();
  2553.     }
  2554.  
  2555.     # Do the text classification and update the counter for that
  2556.     # bucket that we just downloaded an email of that type
  2557.  
  2558.     $classification = ($class ne '')?$class:$self->classify( $session, undef);
  2559.  
  2560.     my $subject_modification = $self->get_bucket_parameter( $session, $classification, 'subject'    );
  2561.     my $xtc_insertion        = $self->get_bucket_parameter( $session, $classification, 'xtc'        );
  2562.     my $xpl_insertion        = $self->get_bucket_parameter( $session, $classification, 'xpl'        );
  2563.     my $quarantine           = $self->get_bucket_parameter( $session, $classification, 'quarantine' );
  2564.  
  2565.     my $modification = $self->config_( 'subject_mod_left' ) . $classification . $self->config_( 'subject_mod_right' );
  2566.  
  2567.     # Add the Subject line modification or the original line back again
  2568.     # Don't add the classification unless it is not present
  2569.  
  2570.     if (  ( defined( $msg_subject ) && ( $msg_subject !~ /\Q$modification\E/ ) ) && # PROFILE BLOCK START
  2571.           ( $subject_modification == 1 ) &&
  2572.           ( $quarantine == 0 ) )  {                                                 # PROFILE BLOCK STOP
  2573.          $msg_subject = " $modification$msg_subject";
  2574.     }
  2575.  
  2576.     if ( !defined( $msg_subject )       &&                                         # PROFILE BLOCK START
  2577.          ( $subject_modification == 1 ) &&
  2578.          ( $quarantine == 0 ) )  {                                                 # PROFILE BLOCK STOP
  2579.          $msg_subject = " $modification";
  2580.     }
  2581.  
  2582.     $msg_subject = '' if ( !defined( $msg_subject ) );
  2583.  
  2584.     $msg_head_before .= 'Subject:' . $msg_subject;
  2585.     $msg_head_before .= $crlf;
  2586.  
  2587.     # Add the XTC header
  2588.     $msg_head_after .= "X-Text-Classification: $classification$crlf" if ( ( $xtc_insertion   ) && # PROFILE BLOCK START
  2589.                                                                           ( $quarantine == 0 ) ); # PROFILE BLOCK STOP
  2590.  
  2591.     # Add the XPL header
  2592.  
  2593.     my $xpl = $self->config_( 'xpl_angle' )?'<':'';
  2594.  
  2595.     $xpl .= "http://";
  2596.     $xpl .= $self->module_config_( 'html', 'local' )?"127.0.0.1":$self->config_( 'hostname' );
  2597.     $xpl .= ":" . $self->module_config_( 'html', 'port' ) . "/jump_to_message?view=$slot";
  2598.  
  2599.     if ( $self->config_( 'xpl_angle' ) ) {
  2600.         $xpl .= '>';
  2601.     }
  2602.  
  2603.     $xpl .= "$crlf";
  2604.  
  2605.     if ( $xpl_insertion && ( $quarantine == 0 ) ) {
  2606.         $msg_head_after .= 'X-POPFile-Link: ' . $xpl;
  2607.     }
  2608.  
  2609.     $msg_head_after .= $msg_head_q . "$crlf";
  2610.  
  2611.     # Echo the text of the message to the client
  2612.  
  2613.     if ( $echo ) {
  2614.  
  2615.         # If the bucket is quarantined then we'll treat it specially
  2616.         # by changing the message header to contain information from
  2617.         # POPFile and wrapping the original message in a MIME encoding
  2618.  
  2619.        if ( $quarantine == 1 ) {
  2620.            my ( $orig_from, $orig_to, $orig_subject ) = ( $self->{parser__}->get_header('from'), $self->{parser__}->get_header('to'), $self->{parser__}->get_header('subject') );
  2621.            my ( $encoded_from, $encoded_to ) = ( $orig_from, $orig_to );
  2622.            if ( $self->{parser__}->{lang__} eq 'Nihongo' ) {
  2623.                require Encode;
  2624.  
  2625.                Encode::from_to( $orig_from, 'euc-jp', 'iso-2022-jp');
  2626.                Encode::from_to( $orig_to, 'euc-jp', 'iso-2022-jp');
  2627.                Encode::from_to( $orig_subject, 'euc-jp', 'iso-2022-jp');
  2628.  
  2629.                $encoded_from = $orig_from;
  2630.                $encoded_to = $orig_to;
  2631.                $encoded_from =~ s/(\x1B\x24\x42.+\x1B\x28\x42)/"=?ISO-2022-JP?B?" . encode_base64($1,'') . "?="/eg;
  2632.                $encoded_to =~ s/(\x1B\x24\x42.+\x1B\x28\x42)/"=?ISO-2022-JP?B?" . encode_base64($1,'') . "?="/eg;
  2633.            }
  2634.  
  2635.            print $client "From: $encoded_from$crlf";
  2636.            print $client "To: $encoded_to$crlf";
  2637.            print $client "Date: " . $self->{parser__}->get_header( 'date' ) . "$crlf";
  2638.            # Don't add the classification unless it is not present
  2639.            if ( ( defined( $msg_subject ) && ( $msg_subject !~ /\[\Q$classification\E\]/ ) ) && # PROFILE BLOCK START
  2640.                  ( $subject_modification == 1 ) ) {                                             # PROFILE BLOCK STOP
  2641.                $msg_subject = " $modification$msg_subject";
  2642.            }
  2643.            print $client "Subject:$msg_subject$crlf";
  2644.            print $client "X-Text-Classification: $classification$crlf" if ( $xtc_insertion );
  2645.            print $client 'X-POPFile-Link: ' . $xpl if ( $xpl_insertion );
  2646.            print $client "MIME-Version: 1.0$crlf";
  2647.            print $client "Content-Type: multipart/report; boundary=\"$slot\"$crlf$crlf--$slot$crlf";
  2648.            print $client "Content-Type: text/plain";
  2649.            print $client "; charset=iso-2022-jp" if ( $self->{parser__}->{lang__} eq 'Nihongo' );
  2650.            print $client "$crlf$crlf";
  2651.            print $client "POPFile has quarantined a message.  It is attached to this email.$crlf$crlf";
  2652.            print $client "Quarantined Message Detail$crlf$crlf";
  2653.  
  2654.            print $client "Original From: $orig_from$crlf";
  2655.            print $client "Original To: $orig_to$crlf";
  2656.            print $client "Original Subject: $orig_subject$crlf";
  2657.  
  2658.            print $client "To examine the email open the attachment. ";
  2659.            print $client "To change this mail's classification go to $xpl";
  2660.            print $client "$crlf";
  2661.            print $client "The first 20 words found in the email are:$crlf$crlf";
  2662.  
  2663.            my $first20 = $self->{parser__}->first20();
  2664.            if ( $self->{parser__}->{lang__} eq 'Nihongo' ) {
  2665.                require Encode;
  2666.  
  2667.                Encode::from_to( $first20, 'euc-jp', 'iso-2022-jp');
  2668.            }
  2669.  
  2670.            print $client $first20;
  2671.            print $client "$crlf--$slot$crlf";
  2672.            print $client "Content-Type: message/rfc822$crlf$crlf";
  2673.         }
  2674.  
  2675.         print $client $msg_head_before;
  2676.         print $client $msg_head_after;
  2677.         print $client $msg_body;
  2678.     }
  2679.  
  2680.     my $before_dot = '';
  2681.  
  2682.     if ( $quarantine && $echo ) {
  2683.         $before_dot = "$crlf--$slot--$crlf";
  2684.     }
  2685.  
  2686.     my $need_dot = 0;
  2687.  
  2688.     if ( $got_full_body ) {
  2689.         $need_dot = 1;
  2690.     } else {
  2691.         $need_dot = !$self->echo_to_dot_( $mail, $echo?$client:undef, $nosave?undef:'>>' . $msg_file, $before_dot ) && !$nosave;
  2692.     }
  2693.  
  2694.     if ( $need_dot ) {
  2695.         print $client $before_dot if ( $before_dot ne '' );
  2696.         print $client ".$crlf"    if ( $echo );
  2697.     }
  2698.  
  2699.     # In some cases it's possible (and totally illegal) to get a . in
  2700.     # the middle of the message, to cope with the we call flush_extra_
  2701.     # here to remove any extra stuff the POP3 server is sending Make
  2702.     # sure to supress output if we are not echoing, and to save to
  2703.     # file if not echoing and saving
  2704.  
  2705.     if ( !($nosave || $echo) ) {
  2706.  
  2707.         # if we're saving (not nosave) and not echoing, we can safely
  2708.         # unload this into the temp file
  2709.  
  2710.         if (open FLUSH, ">$msg_file.flush") {
  2711.             binmode FLUSH;
  2712.  
  2713.             # TODO: Do this in a faster way (without flushing to one
  2714.             # file then copying to another) (perhaps a select on $mail
  2715.             # to predict if there is flushable data)
  2716.  
  2717.             $self->flush_extra_( $mail, \*FLUSH, 0);
  2718.             close FLUSH;
  2719.  
  2720.             # append any data we got to the actual temp file
  2721.  
  2722.             if ( ( (-s "$msg_file.flush") > 0 ) && ( open FLUSH, "<$msg_file.flush" ) ) {
  2723.                 binmode FLUSH;
  2724.                 if ( open TEMP, ">>$msg_file" ) {
  2725.                     binmode TEMP;
  2726.  
  2727.                     # The only time we get data here is if it is after
  2728.                     # a CRLF.CRLF We have to re-create it to avoid
  2729.                     # data-loss
  2730.  
  2731.                     print TEMP ".$crlf";
  2732.  
  2733.                     print TEMP $_ while (<FLUSH>);
  2734.  
  2735.                     # NOTE: The last line flushed MAY be a CRLF.CRLF,
  2736.                     # which isn't actually part of the message body
  2737.  
  2738.                     close TEMP;
  2739.                 }
  2740.                 close FLUSH;
  2741.             }
  2742.             unlink("$msg_file.flush");
  2743.         }
  2744.     } else {
  2745.  
  2746.         # if we are echoing, the client can make sure we have no data
  2747.         # loss otherwise, the data can be discarded (not saved and not
  2748.         # echoed)
  2749.  
  2750.         $self->flush_extra_( $mail, $client, $echo?0:1);
  2751.     }
  2752.  
  2753.     if ( $class eq '' ) {
  2754.         if ( $nosave ) {
  2755.             $self->{history__}->release_slot( $slot );
  2756.         } else {
  2757.             $self->{history__}->commit_slot( $session, $slot, $classification, $self->{magnet_detail__} );
  2758.         }
  2759.     }
  2760.  
  2761.     return ( $classification, $slot, $self->{magnet_used__} );
  2762. }
  2763.  
  2764. #----------------------------------------------------------------------------
  2765. #
  2766. # get_buckets
  2767. #
  2768. # Returns a list containing all the real bucket names sorted into
  2769. # alphabetic order
  2770. #
  2771. # $session   A valid session key returned by a call to get_session_key
  2772. #
  2773. #----------------------------------------------------------------------------
  2774. sub get_buckets
  2775. {
  2776.     my ( $self, $session ) = @_;
  2777.  
  2778.     my $userid = $self->valid_session_key__( $session );
  2779.     return undef if ( !defined( $userid ) );
  2780.  
  2781.     # Note that get_buckets does not return pseudo buckets
  2782.  
  2783.     my @buckets;
  2784.  
  2785.     for my $b (sort keys %{$self->{db_bucketid__}{$userid}}) {
  2786.         if ( $self->{db_bucketid__}{$userid}{$b}{pseudo} == 0 ) {
  2787.             push @buckets, ($b);
  2788.         }
  2789.     }
  2790.  
  2791.     return @buckets;
  2792. }
  2793.  
  2794. #----------------------------------------------------------------------------
  2795. #
  2796. # get_bucket_id
  2797. #
  2798. # Returns the internal ID for a bucket for database calls
  2799. #
  2800. # $session   A valid session key returned by a call to get_session_key
  2801. # $bucket    The bucket name
  2802. #
  2803. #----------------------------------------------------------------------------
  2804. sub get_bucket_id
  2805. {
  2806.     my ( $self, $session, $bucket ) = @_;
  2807.  
  2808.     my $userid = $self->valid_session_key__( $session );
  2809.     return undef if ( !defined( $userid ) );
  2810.  
  2811.     return $self->{db_bucketid__}{$userid}{$bucket}{id};
  2812. }
  2813.  
  2814. #----------------------------------------------------------------------------
  2815. #
  2816. # get_bucket_name
  2817. #
  2818. # Returns the name of a bucket from an internal ID
  2819. #
  2820. # $session   A valid session key returned by a call to get_session_key
  2821. # $id        The bucket id
  2822. #
  2823. #----------------------------------------------------------------------------
  2824. sub get_bucket_name
  2825. {
  2826.     my ( $self, $session, $id ) = @_;
  2827.  
  2828.     my $userid = $self->valid_session_key__( $session );
  2829.     return undef if ( !defined( $userid ) );
  2830.  
  2831.     foreach $b (keys %{$self->{db_bucketid__}{$userid}}) {
  2832.         if ( $id == $self->{db_bucketid__}{$userid}{$b}{id} ) {
  2833.             return $b;
  2834.         }
  2835.     }
  2836.  
  2837.     return '';
  2838. }
  2839.  
  2840. #----------------------------------------------------------------------------
  2841. #
  2842. # get_pseudo_buckets
  2843. #
  2844. # Returns a list containing all the pseudo bucket names sorted into
  2845. # alphabetic order
  2846. #
  2847. # $session   A valid session key returned by a call to get_session_key
  2848. #
  2849. #----------------------------------------------------------------------------
  2850. sub get_pseudo_buckets
  2851. {
  2852.     my ( $self, $session ) = @_;
  2853.  
  2854.     my $userid = $self->valid_session_key__( $session );
  2855.     return undef if ( !defined( $userid ) );
  2856.  
  2857.     my @buckets;
  2858.  
  2859.     for my $b (sort keys %{$self->{db_bucketid__}{$userid}}) {
  2860.         if ( $self->{db_bucketid__}{$userid}{$b}{pseudo} == 1 ) {
  2861.             push @buckets, ($b);
  2862.         }
  2863.     }
  2864.  
  2865.     return @buckets;
  2866. }
  2867.  
  2868. #----------------------------------------------------------------------------
  2869. #
  2870. # get_all_buckets
  2871. #
  2872. # Returns a list containing all the bucket names sorted into
  2873. # alphabetic order
  2874. #
  2875. # $session   A valid session key returned by a call to get_session_key
  2876. #
  2877. #----------------------------------------------------------------------------
  2878. sub get_all_buckets
  2879. {
  2880.     my ( $self, $session ) = @_;
  2881.  
  2882.     my $userid = $self->valid_session_key__( $session );
  2883.     return undef if ( !defined( $userid ) );
  2884.  
  2885.     my @buckets;
  2886.  
  2887.     for my $b (sort keys %{$self->{db_bucketid__}{$userid}}) {
  2888.          push @buckets, ($b);
  2889.     }
  2890.  
  2891.     return @buckets;
  2892. }
  2893.  
  2894. #----------------------------------------------------------------------------
  2895. #
  2896. # is_pseudo_bucket
  2897. #
  2898. # Returns 1 if the named bucket is pseudo
  2899. #
  2900. # $session   A valid session key returned by a call to get_session_key
  2901. # $bucket    The bucket to check
  2902. #
  2903. #----------------------------------------------------------------------------
  2904. sub is_pseudo_bucket
  2905. {
  2906.     my ( $self, $session, $bucket ) = @_;
  2907.  
  2908.     my $userid = $self->valid_session_key__( $session );
  2909.     return undef if ( !defined( $userid ) );
  2910.  
  2911.     return ( defined($self->{db_bucketid__}{$userid}{$bucket})   # PROFILE BLOCK START
  2912.           && $self->{db_bucketid__}{$userid}{$bucket}{pseudo} ); # PROFILE BLOCK STOP
  2913. }
  2914.  
  2915. #----------------------------------------------------------------------------
  2916. #
  2917. # is_bucket
  2918. #
  2919. # Returns 1 if the named bucket is a bucket
  2920. #
  2921. # $session   A valid session key returned by a call to get_session_key
  2922. # $bucket    The bucket to check
  2923. #
  2924. #----------------------------------------------------------------------------
  2925. sub is_bucket
  2926. {
  2927.     my ( $self, $session, $bucket ) = @_;
  2928.  
  2929.     my $userid = $self->valid_session_key__( $session );
  2930.     return undef if ( !defined( $userid ) );
  2931.  
  2932.     return ( ( defined( $self->{db_bucketid__}{$userid}{$bucket} ) ) &&  # PROFILE BLOCK START
  2933.              ( !$self->{db_bucketid__}{$userid}{$bucket}{pseudo} ) );    # PROFILE BLOCK STOP
  2934. }
  2935.  
  2936. #----------------------------------------------------------------------------
  2937. #
  2938. # get_bucket_word_count
  2939. #
  2940. # Returns the total word count (including duplicates) for the passed in bucket
  2941. #
  2942. # $session     A valid session key returned by a call to get_session_key
  2943. # $bucket      The name of the bucket for which the word count is desired
  2944. #
  2945. #----------------------------------------------------------------------------
  2946. sub get_bucket_word_count
  2947. {
  2948.     my ( $self, $session, $bucket ) = @_;
  2949.  
  2950.     my $userid = $self->valid_session_key__( $session );
  2951.     return undef if ( !defined( $userid ) );
  2952.  
  2953.     my $c = $self->{db_bucketcount__}{$userid}{$bucket};
  2954.  
  2955.     return defined($c)?$c:0;
  2956. }
  2957.  
  2958. #----------------------------------------------------------------------------
  2959. #
  2960. # get_bucket_word_list
  2961. #
  2962. # Returns a list of words all with the same first character
  2963. #
  2964. # $session     A valid session key returned by a call to get_session_key
  2965. # $bucket      The name of the bucket for which the word count is desired
  2966. # $prefix      The first character of the words
  2967. #
  2968. #----------------------------------------------------------------------------
  2969. sub get_bucket_word_list
  2970. {
  2971.     my ( $self, $session, $bucket, $prefix ) = @_;
  2972.  
  2973.     my $userid = $self->valid_session_key__( $session );
  2974.     return undef if ( !defined( $userid ) );
  2975.  
  2976.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  2977.     my $result = $self->{db__}->selectcol_arrayref(  # PROFILE BLOCK START
  2978.         "select words.word from matrix, words
  2979.          where matrix.wordid  = words.id and
  2980.                matrix.bucketid = $bucketid and
  2981.                words.word like '$prefix%';");        # PROFILE BLOCK STOP
  2982.  
  2983.     return @{$result};
  2984. }
  2985.  
  2986. #----------------------------------------------------------------------------
  2987. #
  2988. # get_bucket_word_prefixes
  2989. #
  2990. # Returns a list of all the initial letters of words in a bucket
  2991. #
  2992. # $session     A valid session key returned by a call to get_session_key
  2993. # $bucket      The name of the bucket for which the word count is desired
  2994. #
  2995. #----------------------------------------------------------------------------
  2996. sub get_bucket_word_prefixes
  2997. {
  2998.     my ( $self, $session, $bucket ) = @_;
  2999.  
  3000.     my $userid = $self->valid_session_key__( $session );
  3001.     return undef if ( !defined( $userid ) );
  3002.  
  3003.     my $prev = '';
  3004.  
  3005.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  3006.     my $result = $self->{db__}->selectcol_arrayref(   # PROFILE BLOCK START
  3007.         "select words.word from matrix, words
  3008.          where matrix.wordid  = words.id and
  3009.                matrix.bucketid = $bucketid;");        # PROFILE BLOCK STOP
  3010.  
  3011.     # In Japanese mode, disable locale and use substr_euc, the substr
  3012.     # function which supports EUC Japanese charset.  Sorting Japanese
  3013.     # with "use locale" is memory and time consuming, and may cause
  3014.     # perl crash.
  3015.  
  3016.     if ( $self->module_config_( 'html', 'language' ) eq 'Nihongo' ) {
  3017.         return grep {$_ ne $prev && ($prev = $_, 1)} sort map {substr_euc__($_,0,1)} @{$result};
  3018.     } else {
  3019.         if  ( $self->module_config_( 'html', 'language' ) eq 'Korean' ) {
  3020.             return grep {$_ ne $prev && ($prev = $_, 1)} sort map {$_ =~ /([\x20-\x80]|$eksc)/} @{$result};
  3021.         } else {
  3022.             return grep {$_ ne $prev && ($prev = $_, 1)} sort map {substr($_,0,1)}  @{$result};
  3023.         }
  3024.     }
  3025. }
  3026.  
  3027. #----------------------------------------------------------------------------
  3028. #
  3029. # get_word_count
  3030. #
  3031. # Returns the total word count (including duplicates)
  3032. #
  3033. # $session   A valid session key returned by a call to get_session_key
  3034. #
  3035. #----------------------------------------------------------------------------
  3036. sub get_word_count
  3037. {
  3038.     my ( $self, $session ) = @_;
  3039.  
  3040.     my $userid = $self->valid_session_key__( $session );
  3041.     return undef if ( !defined( $userid ) );
  3042.  
  3043.     $self->{db_get_full_total__}->execute( $userid );
  3044.     return $self->{db_get_full_total__}->fetchrow_arrayref->[0];
  3045. }
  3046.  
  3047. #----------------------------------------------------------------------------
  3048. #
  3049. # get_count_for_word
  3050. #
  3051. # Returns the number of times the word occurs in a bucket
  3052. #
  3053. # $session         A valid session key returned by a call to get_session_key
  3054. # $bucket          The bucket we are asking about
  3055. # $word            The word we are asking about
  3056. #
  3057. #----------------------------------------------------------------------------
  3058. sub get_count_for_word
  3059. {
  3060.     my ( $self, $session, $bucket, $word ) = @_;
  3061.  
  3062.     my $userid = $self->valid_session_key__( $session );
  3063.     return undef if ( !defined( $userid ) );
  3064.  
  3065.     return $self->get_base_value_( $session, $bucket, $word );
  3066. }
  3067.  
  3068. #----------------------------------------------------------------------------
  3069. #
  3070. # get_bucket_unique_count
  3071. #
  3072. # Returns the unique word count (excluding duplicates) for the passed
  3073. # in bucket
  3074. #
  3075. # $session     A valid session key returned by a call to get_session_key
  3076. # $bucket      The name of the bucket for which the word count is desired
  3077. #
  3078. #----------------------------------------------------------------------------
  3079. sub get_bucket_unique_count
  3080. {
  3081.     my ( $self, $session, $bucket ) = @_;
  3082.  
  3083.     my $userid = $self->valid_session_key__( $session );
  3084.     return undef if ( !defined( $userid ) );
  3085.  
  3086.     my $c = $self->{db_bucketunique__}{$userid}{$bucket};
  3087.  
  3088.     return defined($c)?$c:0;
  3089. }
  3090.  
  3091. #----------------------------------------------------------------------------
  3092. #
  3093. # get_unique_word_count
  3094. #
  3095. # Returns the unique word count (excluding duplicates) for all buckets
  3096. #
  3097. # $session   A valid session key returned by a call to get_session_key
  3098. #
  3099. #----------------------------------------------------------------------------
  3100. sub get_unique_word_count
  3101. {
  3102.     my ( $self, $session ) = @_;
  3103.  
  3104.     my $userid = $self->valid_session_key__( $session );
  3105.     return undef if ( !defined( $userid ) );
  3106.  
  3107.     $self->{db_get_unique_word_count__}->execute( $userid );
  3108.     return $self->{db_get_unique_word_count__}->fetchrow_arrayref->[0];
  3109. }
  3110.  
  3111. #----------------------------------------------------------------------------
  3112. #
  3113. # get_bucket_color
  3114. #
  3115. # Returns the color associated with a bucket
  3116. #
  3117. # $session   A valid session key returned by a call to get_session_key
  3118. # $bucket      The name of the bucket for which the color is requested
  3119. #
  3120. # NOTE  This API is DEPRECATED in favor of calling get_bucket_parameter for
  3121. #       the parameter named 'color'
  3122. #----------------------------------------------------------------------------
  3123. sub get_bucket_color
  3124. {
  3125.     my ( $self, $session, $bucket ) = @_;
  3126.  
  3127.     return $self->get_bucket_parameter( $session, $bucket, 'color' );
  3128. }
  3129.  
  3130. #----------------------------------------------------------------------------
  3131. #
  3132. # set_bucket_color
  3133. #
  3134. # Returns the color associated with a bucket
  3135. #
  3136. # $session     A valid session key returned by a call to get_session_key
  3137. # $bucket      The name of the bucket for which the color is requested
  3138. # $color       The new color
  3139. #
  3140. # NOTE  This API is DEPRECATED in favor of calling set_bucket_parameter for
  3141. #       the parameter named 'color'
  3142. #----------------------------------------------------------------------------
  3143. sub set_bucket_color
  3144. {
  3145.     my ( $self, $session, $bucket, $color ) = @_;
  3146.  
  3147.     return $self->set_bucket_parameter( $session, $bucket, 'color', $color );
  3148. }
  3149.  
  3150. #----------------------------------------------------------------------------
  3151. #
  3152. # get_bucket_parameter
  3153. #
  3154. # Returns the value of a per bucket parameter
  3155. #
  3156. # $session     A valid session key returned by a call to get_session_key
  3157. # $bucket      The name of the bucket
  3158. # $parameter   The name of the parameter
  3159. #
  3160. #----------------------------------------------------------------------------
  3161. sub get_bucket_parameter
  3162. {
  3163.     my ( $self, $session, $bucket, $parameter ) = @_;
  3164.  
  3165.     my $userid = $self->valid_session_key__( $session );
  3166.     return undef if ( !defined( $userid ) );
  3167.  
  3168.     # See if there's a cached value
  3169.  
  3170.     if ( defined( $self->{db_parameters__}{$userid}{$bucket}{$parameter} ) ) {
  3171.         return $self->{db_parameters__}{$userid}{$bucket}{$parameter};
  3172.     }
  3173.  
  3174.     # Make sure that the bucket passed in actually exists
  3175.  
  3176.     if ( !defined( $self->{db_bucketid__}{$userid}{$bucket} ) ) {
  3177.         return undef;
  3178.     }
  3179.  
  3180.     # If there is a non-default value for this parameter then return it.
  3181.  
  3182.     $self->{db_get_bucket_parameter__}->execute( $self->{db_bucketid__}{$userid}{$bucket}{id}, $self->{db_parameterid__}{$parameter} );
  3183.     my $result = $self->{db_get_bucket_parameter__}->fetchrow_arrayref;
  3184.  
  3185.     # If this parameter has not been defined for this specific bucket then
  3186.     # get the default value
  3187.  
  3188.     if ( !defined( $result ) ) {
  3189.         $self->{db_get_bucket_parameter_default__}->execute(  # PROFILE BLOCK START
  3190.             $self->{db_parameterid__}{$parameter} );          # PROFILE BLOCK STOP
  3191.         $result = $self->{db_get_bucket_parameter_default__}->fetchrow_arrayref;
  3192.     }
  3193.  
  3194.     if ( defined( $result ) ) {
  3195.         $self->{db_parameters__}{$userid}{$bucket}{$parameter} = $result->[0];
  3196.         return $result->[0];
  3197.     } else {
  3198.         return undef;
  3199.     }
  3200. }
  3201.  
  3202. #----------------------------------------------------------------------------
  3203. #
  3204. # set_bucket_parameter
  3205. #
  3206. # Sets the value associated with a bucket specific parameter
  3207. #
  3208. # $session     A valid session key returned by a call to get_session_key
  3209. # $bucket      The name of the bucket
  3210. # $parameter   The name of the parameter
  3211. # $value       The new value
  3212. #
  3213. #----------------------------------------------------------------------------
  3214. sub set_bucket_parameter
  3215. {
  3216.     my ( $self, $session, $bucket, $parameter, $value ) = @_;
  3217.  
  3218.     my $userid = $self->valid_session_key__( $session );
  3219.     return undef if ( !defined( $userid ) );
  3220.  
  3221.     # Make sure that the bucket passed in actually exists
  3222.  
  3223.     if ( !defined( $self->{db_bucketid__}{$userid}{$bucket} ) ) {
  3224.         return undef;
  3225.     }
  3226.  
  3227.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  3228.     my $btid     = $self->{db_parameterid__}{$parameter};
  3229.  
  3230.     # Exactly one row should be affected by this statement
  3231.  
  3232.     $self->{db_set_bucket_parameter__}->execute( $bucketid, $btid, $value );
  3233.  
  3234.     if ( defined( $self->{db_parameters__}{$userid}{$bucket}{$parameter} ) ) {
  3235.         $self->{db_parameters__}{$userid}{$bucket}{$parameter} = $value;
  3236.     }
  3237.  
  3238.     return 1;
  3239. }
  3240.  
  3241. #----------------------------------------------------------------------------
  3242. #
  3243. # get_html_colored_message
  3244. #
  3245. # Parser a mail message stored in a file and returns HTML representing
  3246. # the message with coloring of the words
  3247. #
  3248. # $session        A valid session key returned by a call to get_session_key
  3249. # $file           The file to parse
  3250. #
  3251. #----------------------------------------------------------------------------
  3252. sub get_html_colored_message
  3253. {
  3254.     my ( $self, $session, $file ) = @_;
  3255.  
  3256.     my $userid = $self->valid_session_key__( $session );
  3257.     return undef if ( !defined( $userid ) );
  3258.  
  3259.     $self->{parser__}->{color__} = $session;
  3260.     $self->{parser__}->{color_matrix__} = undef;
  3261.     $self->{parser__}->{color_idmap__}  = undef;
  3262.     $self->{parser__}->{color_userid__} = undef;
  3263.     $self->{parser__}->{bayes__} = bless $self;
  3264.     
  3265.     my $result = $self->{parser__}->parse_file( $file,   # PROFILE BLOCK START
  3266.            $self->global_config_( 'message_cutoff'   ) ); # PROFILE BLOCK STOP
  3267.  
  3268.     $self->{parser__}->{color__} = '';
  3269.  
  3270.     return $result;
  3271. }
  3272.  
  3273. #----------------------------------------------------------------------------
  3274. #
  3275. # fast_get_html_colored_message
  3276. #
  3277. # Parser a mail message stored in a file and returns HTML representing the message
  3278. # with coloring of the words
  3279. #
  3280. # $session        A valid session key returned by a call to get_session_key
  3281. # $file           The file to colorize
  3282. # $matrix         Reference to the matrix hash from a call to classify
  3283. # $idmap          Reference to the idmap hash from a call to classify
  3284. #
  3285. #----------------------------------------------------------------------------
  3286. sub fast_get_html_colored_message
  3287. {
  3288.     my ( $self, $session, $file, $matrix, $idmap ) = @_;
  3289.  
  3290.     my $userid = $self->valid_session_key__( $session );
  3291.     return undef if ( !defined( $userid ) );
  3292.  
  3293.     $self->{parser__}->{color__}        = $session;
  3294.     $self->{parser__}->{color_matrix__} = $matrix;
  3295.     $self->{parser__}->{color_idmap__}  = $idmap;
  3296.     $self->{parser__}->{color_userid__} = $userid;
  3297.     $self->{parser__}->{bayes__}        = bless $self;
  3298.  
  3299.     my $result = $self->{parser__}->parse_file( $file,
  3300.                                                 $self->global_config_( 'message_cutoff'   ) );
  3301.  
  3302.     $self->{parser__}->{color__} = '';
  3303.  
  3304.     return $result;
  3305. }
  3306.  
  3307. #----------------------------------------------------------------------------
  3308. #
  3309. # create_bucket
  3310. #
  3311. # Creates a new bucket, returns 1 if the creation succeeded
  3312. #
  3313. # $session     A valid session key returned by a call to get_session_key
  3314. # $bucket      Name for the new bucket
  3315. #
  3316. #----------------------------------------------------------------------------
  3317. sub create_bucket
  3318. {
  3319.     my ( $self, $session, $bucket ) = @_;
  3320.  
  3321.     if ( $self->is_bucket( $session, $bucket ) ||           # PROFILE BLOCK START
  3322.          $self->is_pseudo_bucket( $session, $bucket ) ) {   # PROFILE BLOCK STOP
  3323.         return 0;
  3324.     }
  3325.  
  3326.     my $userid = $self->valid_session_key__( $session );
  3327.     return undef if ( !defined( $userid ) );
  3328.  
  3329.     $bucket = $self->{db__}->quote( $bucket );
  3330.  
  3331.     $self->{db__}->do(                                                                    # PROFILE BLOCK START
  3332.         "insert into buckets ( name, pseudo, userid ) values ( $bucket, 0, $userid );" ); # PROFILE BLOCK STOP
  3333.     $self->db_update_cache__( $session );
  3334.  
  3335.     return 1;
  3336. }
  3337.  
  3338. #----------------------------------------------------------------------------
  3339. #
  3340. # delete_bucket
  3341. #
  3342. # Deletes a bucket, returns 1 if the delete succeeded
  3343. #
  3344. # $session     A valid session key returned by a call to get_session_key
  3345. # $bucket      Name of the bucket to delete
  3346. #
  3347. #----------------------------------------------------------------------------
  3348. sub delete_bucket
  3349. {
  3350.     my ( $self, $session, $bucket ) = @_;
  3351.  
  3352.     my $userid = $self->valid_session_key__( $session );
  3353.     return undef if ( !defined( $userid ) );
  3354.  
  3355.     # Make sure that the bucket passed in actually exists
  3356.  
  3357.     if ( !defined( $self->{db_bucketid__}{$userid}{$bucket} ) ) {
  3358.         return 0;
  3359.     }
  3360.  
  3361.     $self->{db__}->do(                                                                        # PROFILE BLOCK START
  3362.         "delete from buckets where buckets.userid = $userid and buckets.name = '$bucket';" ); # PROFILE BLOCK STOP
  3363.     $self->db_update_cache__( $session );
  3364.  
  3365.     return 1;
  3366. }
  3367.  
  3368. #----------------------------------------------------------------------------
  3369. #
  3370. # rename_bucket
  3371. #
  3372. # Renames a bucket, returns 1 if the rename succeeded
  3373. #
  3374. # $session             A valid session key returned by a call to get_session_key
  3375. # $old_bucket          The old name of the bucket
  3376. # $new_bucket          The new name of the bucket
  3377. #
  3378. #----------------------------------------------------------------------------
  3379. sub rename_bucket
  3380. {
  3381.     my ( $self, $session, $old_bucket, $new_bucket ) = @_;
  3382.  
  3383.     my $userid = $self->valid_session_key__( $session );
  3384.     return undef if ( !defined( $userid ) );
  3385.  
  3386.     # Make sure that the bucket passed in actually exists
  3387.  
  3388.     if ( !defined( $self->{db_bucketid__}{$userid}{$old_bucket} ) ) {
  3389.         $self->log_( 0, "Bad bucket name $old_bucket to rename_bucket" );
  3390.         return 0;
  3391.     }
  3392.  
  3393.     my $id = $self->{db__}->quote( $self->{db_bucketid__}{$userid}{$old_bucket}{id} );
  3394.     $new_bucket = $self->{db__}->quote( $new_bucket );
  3395.  
  3396.     $self->log_( 1, "Rename bucket $old_bucket to $new_bucket" );
  3397.  
  3398.     my $result = $self->{db__}->do( "update buckets set name = $new_bucket where id = $id;" );
  3399.  
  3400.     if ( !defined( $result ) || ( $result == -1 ) ) {
  3401.         return 0;
  3402.     } else {
  3403.         $self->db_update_cache__( $session );
  3404.         return 1;
  3405.     }
  3406. }
  3407.  
  3408. #----------------------------------------------------------------------------
  3409. #
  3410. # add_messages_to_bucket
  3411. #
  3412. # Parses mail messages and updates the statistics in the specified bucket
  3413. #
  3414. # $session         A valid session key returned by a call to get_session_key
  3415. # $bucket          Name of the bucket to be updated
  3416. # @files           List of file names to parse
  3417. #
  3418. #----------------------------------------------------------------------------
  3419. sub add_messages_to_bucket
  3420. {
  3421.     my ( $self, $session, $bucket, @files ) = @_;
  3422.  
  3423.     my $userid = $self->valid_session_key__( $session );
  3424.     return undef if ( !defined( $userid ) );
  3425.  
  3426.     if ( !defined( $self->{db_bucketid__}{$userid}{$bucket}{id} ) ) {
  3427.         return 0;
  3428.     }
  3429.  
  3430.     # This is done to clear out the word list because in the loop
  3431.     # below we are going to not reset the word list on each parse
  3432.  
  3433.     $self->{parser__}->start_parse();
  3434.     $self->{parser__}->stop_parse();
  3435.  
  3436.     foreach my $file (@files) {
  3437.         $self->{parser__}->parse_file( $file,  # PROFILE BLOCK START
  3438.             $self->global_config_( 'message_cutoff'   ),
  3439.             0 );  # PROFILE BLOCK STOP (Do not reset word list)
  3440.     }
  3441.  
  3442.     $self->add_words_to_bucket__( $session, $bucket, 1 );
  3443.     $self->db_update_cache__( $session );
  3444.  
  3445.     return 1;
  3446. }
  3447.  
  3448. #----------------------------------------------------------------------------
  3449. #
  3450. # add_message_to_bucket
  3451. #
  3452. # Parses a mail message and updates the statistics in the specified bucket
  3453. #
  3454. # $session         A valid session key returned by a call to get_session_key
  3455. # $bucket          Name of the bucket to be updated
  3456. # $file            Name of file containing mail message to parse
  3457. #
  3458. #----------------------------------------------------------------------------
  3459. sub add_message_to_bucket
  3460. {
  3461.     my ( $self, $session, $bucket, $file ) = @_;
  3462.  
  3463.     my $userid = $self->valid_session_key__( $session );
  3464.     return undef if ( !defined( $userid ) );
  3465.  
  3466.     return $self->add_messages_to_bucket( $session, $bucket, $file );
  3467. }
  3468.  
  3469. #----------------------------------------------------------------------------
  3470. #
  3471. # remove_message_from_bucket
  3472. #
  3473. # Parses a mail message and updates the statistics in the specified bucket
  3474. #
  3475. # $session         A valid session key returned by a call to get_session_key
  3476. # $bucket          Name of the bucket to be updated
  3477. # $file            Name of file containing mail message to parse
  3478. #
  3479. #----------------------------------------------------------------------------
  3480. sub remove_message_from_bucket
  3481. {
  3482.     my ( $self, $session, $bucket, $file ) = @_;
  3483.  
  3484.     my $userid = $self->valid_session_key__( $session );
  3485.     return undef if ( !defined( $userid ) );
  3486.  
  3487.     $self->{parser__}->parse_file( $file,               # PROFILE BLOCK START
  3488.          $self->global_config_( 'message_cutoff'   ) ); # PROFILE BLOCK STOP
  3489.     $self->add_words_to_bucket__( $session, $bucket, -1 );
  3490.  
  3491.     $self->db_update_cache__( $session );
  3492.  
  3493.     return 1;
  3494. }
  3495.  
  3496. #----------------------------------------------------------------------------
  3497. #
  3498. # get_buckets_with_magnets
  3499. #
  3500. # Returns the names of the buckets for which magnets are defined
  3501. #
  3502. # $session     A valid session key returned by a call to get_session_key
  3503. #
  3504. #----------------------------------------------------------------------------
  3505. sub get_buckets_with_magnets
  3506. {
  3507.     my ( $self, $session ) = @_;
  3508.  
  3509.     my $userid = $self->valid_session_key__( $session );
  3510.     return undef if ( !defined( $userid ) );
  3511.  
  3512.     my @result;
  3513.  
  3514.     $self->{db_get_buckets_with_magnets__}->execute( $userid );
  3515.     while ( my $row = $self->{db_get_buckets_with_magnets__}->fetchrow_arrayref ) {
  3516.         push @result, ($row->[0]);
  3517.     }
  3518.  
  3519.     return @result;
  3520. }
  3521.  
  3522. #----------------------------------------------------------------------------
  3523. #
  3524. # get_magnet_types_in_bucket
  3525. #
  3526. # Returns the types of the magnets in a specific bucket
  3527. #
  3528. # $session     A valid session key returned by a call to get_session_key
  3529. # $bucket      The bucket to search for magnets
  3530. #
  3531. #----------------------------------------------------------------------------
  3532. sub get_magnet_types_in_bucket
  3533. {
  3534.     my ( $self, $session, $bucket ) = @_;
  3535.  
  3536.     my $userid = $self->valid_session_key__( $session );
  3537.     return undef if ( !defined( $userid ) );
  3538.  
  3539.     my @result;
  3540.  
  3541.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  3542.     my $h = $self->{db__}->prepare( "select magnet_types.mtype from magnet_types, magnets, buckets
  3543.         where magnet_types.id = magnets.mtid and
  3544.               magnets.bucketid = buckets.id and
  3545.               buckets.id = $bucketid
  3546.               group by magnet_types.mtype
  3547.               order by magnet_types.mtype;" );
  3548.  
  3549.     $h->execute;
  3550.     while ( my $row = $h->fetchrow_arrayref ) {
  3551.         push @result, ($row->[0]);
  3552.     }
  3553.     $h->finish;
  3554.  
  3555.     return @result;
  3556. }
  3557.  
  3558. #----------------------------------------------------------------------------
  3559. #
  3560. # clear_bucket
  3561. #
  3562. # Removes all words from a bucket
  3563. #
  3564. # $session        A valid session key returned by a call to get_session_key
  3565. # $bucket         The bucket to clear
  3566. #
  3567. #----------------------------------------------------------------------------
  3568. sub clear_bucket
  3569. {
  3570.     my ( $self, $session, $bucket ) = @_;
  3571.  
  3572.     my $userid = $self->valid_session_key__( $session );
  3573.     return undef if ( !defined( $userid ) );
  3574.  
  3575.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  3576.  
  3577.     $self->{db__}->do( "delete from matrix where matrix.bucketid = $bucketid;" );
  3578.     $self->db_update_cache__( $session );
  3579. }
  3580.  
  3581. #----------------------------------------------------------------------------
  3582. #
  3583. # clear_magnets
  3584. #
  3585. # Removes every magnet currently defined
  3586. #
  3587. # $session     A valid session key returned by a call to get_session_key
  3588. #
  3589. #----------------------------------------------------------------------------
  3590. sub clear_magnets
  3591. {
  3592.     my ( $self, $session ) = @_;
  3593.  
  3594.     my $userid = $self->valid_session_key__( $session );
  3595.     return undef if ( !defined( $userid ) );
  3596.  
  3597.     for my $bucket (keys %{$self->{db_bucketid__}{$userid}}) {
  3598.         my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  3599.         $self->{db__}->do( "delete from magnets where magnets.bucketid = $bucketid" );
  3600.     }
  3601. }
  3602.  
  3603. #----------------------------------------------------------------------------
  3604. #
  3605. # get_magnets
  3606. #
  3607. # Returns the magnets of a certain type in a bucket
  3608. #
  3609. # $session         A valid session key returned by a call to get_session_key
  3610. # $bucket          The bucket to search for magnets
  3611. # $type            The magnet type (e.g. from, to or subject)
  3612. #
  3613. #----------------------------------------------------------------------------
  3614. sub get_magnets
  3615. {
  3616.     my ( $self, $session, $bucket, $type ) = @_;
  3617.  
  3618.     my $userid = $self->valid_session_key__( $session );
  3619.     return undef if ( !defined( $userid ) );
  3620.  
  3621.     my @result;
  3622.  
  3623.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  3624.     my $h = $self->{db__}->prepare( "select magnets.val from magnets, magnet_types
  3625.         where magnets.bucketid = $bucketid and
  3626.               magnets.id != 0 and
  3627.               magnet_types.id = magnets.mtid and
  3628.               magnet_types.mtype = '$type' order by magnets.val;" );
  3629.  
  3630.     $h->execute;
  3631.     while ( my $row = $h->fetchrow_arrayref ) {
  3632.         push @result, ($row->[0]);
  3633.     }
  3634.     $h->finish;
  3635.  
  3636.     return @result;
  3637. }
  3638.  
  3639. #----------------------------------------------------------------------------
  3640. #
  3641. # create_magnet
  3642. #
  3643. # Make a new magnet
  3644. #
  3645. # $session         A valid session key returned by a call to get_session_key
  3646. # $bucket          The bucket the magnet belongs in
  3647. # $type            The magnet type (e.g. from, to or subject)
  3648. # $text            The text of the magnet
  3649. #
  3650. #----------------------------------------------------------------------------
  3651. sub create_magnet
  3652. {
  3653.     my ( $self, $session, $bucket, $type, $text ) = @_;
  3654.  
  3655.     my $userid = $self->valid_session_key__( $session );
  3656.     return undef if ( !defined( $userid ) );
  3657.  
  3658.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  3659.     my $result = $self->{db__}->selectrow_arrayref("select magnet_types.id from magnet_types
  3660.                                                         where magnet_types.mtype = '$type';" );
  3661.  
  3662.     my $mtid = $result->[0];
  3663.  
  3664.     $text = $self->{db__}->quote( $text );
  3665.  
  3666.     $self->{db__}->do( "insert into magnets ( bucketid, mtid, val )
  3667.                                      values ( $bucketid, $mtid, $text );" );
  3668. }
  3669.  
  3670. #----------------------------------------------------------------------------
  3671. #
  3672. # get_magnet_types
  3673. #
  3674. # Get a hash mapping magnet types (e.g. from) to magnet names (e.g. From);
  3675. #
  3676. # $session     A valid session key returned by a call to get_session_key
  3677. #
  3678. #----------------------------------------------------------------------------
  3679. sub get_magnet_types
  3680. {
  3681.     my ( $self, $session ) = @_;
  3682.  
  3683.     my $userid = $self->valid_session_key__( $session );
  3684.     return undef if ( !defined( $userid ) );
  3685.  
  3686.     my %result;
  3687.  
  3688.     my $h = $self->{db__}->prepare( "select magnet_types.mtype, magnet_types.header from magnet_types order by mtype;" );
  3689.  
  3690.     $h->execute;
  3691.     while ( my $row = $h->fetchrow_arrayref ) {
  3692.         $result{$row->[0]} = $row->[1];
  3693.     }
  3694.     $h->finish;
  3695.  
  3696.     return %result;
  3697. }
  3698.  
  3699. #----------------------------------------------------------------------------
  3700. #
  3701. # delete_magnet
  3702. #
  3703. # Remove a new magnet
  3704. #
  3705. # $session         A valid session key returned by a call to get_session_key
  3706. # $bucket          The bucket the magnet belongs in
  3707. # $type            The magnet type (e.g. from, to or subject)
  3708. # $text            The text of the magnet
  3709. #
  3710. #----------------------------------------------------------------------------
  3711. sub delete_magnet
  3712. {
  3713.     my ( $self, $session, $bucket, $type, $text ) = @_;
  3714.  
  3715.     my $userid = $self->valid_session_key__( $session );
  3716.     return undef if ( !defined( $userid ) );
  3717.  
  3718.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  3719.     my $result = $self->{db__}->selectrow_arrayref("select magnet_types.id from magnet_types
  3720.                                                         where magnet_types.mtype = '$type';" );
  3721.  
  3722.     my $mtid = $result->[0];
  3723.  
  3724.     $self->{db__}->do( "delete from magnets
  3725.                             where magnets.bucketid = $bucketid and
  3726.                                   magnets.mtid = $mtid and
  3727.                                   magnets.val  = '$text';" );
  3728. }
  3729.  
  3730. #----------------------------------------------------------------------------
  3731. #
  3732. # get_stopword_list
  3733. #
  3734. # Gets the complete list of stop words
  3735. #
  3736. # $session     A valid session key returned by a call to get_session_key
  3737. #
  3738. #----------------------------------------------------------------------------
  3739. sub get_stopword_list
  3740. {
  3741.     my ( $self, $session ) = @_;
  3742.  
  3743.     my $userid = $self->valid_session_key__( $session );
  3744.     return undef if ( !defined( $userid ) );
  3745.  
  3746.     return $self->{parser__}->{mangle__}->stopwords();
  3747. }
  3748.  
  3749. #----------------------------------------------------------------------------
  3750. #
  3751. # magnet_count
  3752. #
  3753. # Gets the number of magnets that are defined
  3754. #
  3755. # $session     A valid session key returned by a call to get_session_key
  3756. #
  3757. #----------------------------------------------------------------------------
  3758. sub magnet_count
  3759. {
  3760.     my ( $self, $session ) = @_;
  3761.  
  3762.     my $userid = $self->valid_session_key__( $session );
  3763.     return undef if ( !defined( $userid ) );
  3764.  
  3765.     my $result = $self->{db__}->selectrow_arrayref( "select count(*) from magnets, buckets
  3766.         where buckets.userid = $userid and
  3767.               magnets.id != 0 and
  3768.               magnets.bucketid = buckets.id;" );
  3769.  
  3770.     if ( defined( $result ) ) {
  3771.         return $result->[0];
  3772.     } else {
  3773.         return 0;
  3774.     }
  3775. }
  3776.  
  3777. #----------------------------------------------------------------------------
  3778. #
  3779. # add_stopword, remove_stopword
  3780. #
  3781. # Adds or removes a stop word
  3782. #
  3783. # $session     A valid session key returned by a call to get_session_key
  3784. # $stopword    The word to add or remove
  3785. #
  3786. # Return 0 for a bad stop word, and 1 otherwise
  3787. #
  3788. #----------------------------------------------------------------------------
  3789. sub add_stopword
  3790. {
  3791.     my ( $self, $session, $stopword ) = @_;
  3792.  
  3793.     my $userid = $self->valid_session_key__( $session );
  3794.     return undef if ( !defined( $userid ) );
  3795.  
  3796.     # Pass language parameter to add_stopword()
  3797.  
  3798.     return $self->{parser__}->{mangle__}->add_stopword( $stopword, $self->module_config_( 'html', 'language' ) );
  3799. }
  3800.  
  3801. sub remove_stopword
  3802. {
  3803.     my ( $self, $session, $stopword ) = @_;
  3804.  
  3805.     my $userid = $self->valid_session_key__( $session );
  3806.     return undef if ( !defined( $userid ) );
  3807.  
  3808.     # Pass language parameter to remove_stopword()
  3809.  
  3810.     return $self->{parser__}->{mangle__}->remove_stopword( $stopword, $self->module_config_( 'html', 'language' ) );
  3811. }
  3812.  
  3813. #----------------------------------------------------------------------------
  3814. #----------------------------------------------------------------------------
  3815. # _____   _____   _____  _______ _____        _______   _______  _____  _____
  3816. #|_____] |     | |_____] |______   |   |      |______   |_____| |_____]   |
  3817. #|       |_____| |       |       __|__ |_____ |______   |     | |       __|__
  3818. #
  3819. #----------------------------------------------------------------------------
  3820. #----------------------------------------------------------------------------
  3821.  
  3822. # GETTERS/SETTERS
  3823.  
  3824. sub wordscores
  3825. {
  3826.     my ( $self, $value ) = @_;
  3827.  
  3828.     $self->{wordscores__} = $value if (defined $value);
  3829.     return $self->{wordscores__};
  3830. }
  3831.  
  3832. sub wmformat
  3833. {
  3834.     my ( $self, $value ) = @_;
  3835.  
  3836.     $self->{wmformat__} = $value if (defined $value);
  3837.     return $self->{wmformat__};
  3838. }
  3839.  
  3840. sub db
  3841. {
  3842.     my ( $self ) = @_;
  3843.  
  3844.     return $self->{db__};
  3845. }
  3846.  
  3847. sub history
  3848. {
  3849.     my ( $self, $history ) = @_;
  3850.  
  3851.     $self->{history__} = $history;
  3852. }
  3853.  
  3854. 1;
  3855.  
  3856.