diff --git a/src/add-ons/mail_daemon/inbound_filters/spam_filter/Jamfile b/src/add-ons/mail_daemon/inbound_filters/spam_filter/Jamfile index 1d76e595d7..9e3d51985a 100644 --- a/src/add-ons/mail_daemon/inbound_filters/spam_filter/Jamfile +++ b/src/add-ons/mail_daemon/inbound_filters/spam_filter/Jamfile @@ -13,3 +13,7 @@ LinkSharedOSLibs Spam\ Filter : Package haiku-maildaemon-cvs : Spam\ Filter : boot home config add-ons mail_daemon inbound_filters ; + +Package haiku-maildaemon-cvs : + spamfilter : + boot home config bin ; \ No newline at end of file diff --git a/src/add-ons/mail_daemon/inbound_filters/spam_filter/SpamFilter.cpp b/src/add-ons/mail_daemon/inbound_filters/spam_filter/SpamFilter.cpp index 0eee2d4d7c..7536f30886 100644 --- a/src/add-ons/mail_daemon/inbound_filters/spam_filter/SpamFilter.cpp +++ b/src/add-ons/mail_daemon/inbound_filters/spam_filter/SpamFilter.cpp @@ -11,6 +11,9 @@ * Public Domain 2002, by Alexander G. M. Smith, no warranty. * * $Log: SpamFilter.cpp,v $ + * Revision 1.2 2004/11/12 02:55:05 nwhitehorn + * Added AGMS's excellent spam detection software. Still some weirdness with the configuration interface from E-mail prefs. + * * Revision 1.1 2004/10/30 22:23:26 brunoga * AGMS Spam Filter. * @@ -104,6 +107,8 @@ #include #include #include +#include +#include #include #include @@ -220,8 +225,22 @@ AGMSBayesianSpamFilter::ProcessMailMessage ( // Make sure the server is running. if (!be_roster->IsRunning (kServerSignature)) { errorCode = be_roster->Launch (kServerSignature); - if (errorCode != B_OK) - goto ErrorExit; + if (errorCode != B_OK) { + BPath path; + entry_ref ref; + directory_which places[] = {B_COMMON_BIN_DIRECTORY,B_BEOS_BIN_DIRECTORY}; + for (int32 i = 0; i < 2; i++) { + find_directory(places[i],&path); + path.Append("spamfilter"); + if (!BEntry(path.Path()).Exists()) + continue; + get_ref_for_path(path.Path(),&ref); + if ((errorCode = be_roster->Launch (&ref)) == B_OK) + break; + } + if (errorCode != B_OK) + goto ErrorExit; + } } // Set up the messenger to the database server. diff --git a/src/add-ons/mail_daemon/inbound_filters/spam_filter/SpamFilterConfig.cpp b/src/add-ons/mail_daemon/inbound_filters/spam_filter/SpamFilterConfig.cpp index 179e790839..e5c0683ada 100644 --- a/src/add-ons/mail_daemon/inbound_filters/spam_filter/SpamFilterConfig.cpp +++ b/src/add-ons/mail_daemon/inbound_filters/spam_filter/SpamFilterConfig.cpp @@ -3,6 +3,9 @@ * settings related to the add-on, but not the server. * * $Log: SpamFilterConfig.cpp,v $ + * Revision 1.2 2004/11/12 02:55:05 nwhitehorn + * Added AGMS's excellent spam detection software. Still some weirdness with the configuration interface from E-mail prefs. + * * Revision 1.1 2004/10/30 22:23:26 brunoga * AGMS Spam Filter. * @@ -71,6 +74,9 @@ #include #include #include +#include +#include +#include #include #include @@ -359,17 +365,30 @@ AGMSBayesianSpamFilterConfig::ShowSpamServerConfigurationWindow () { // Make sure the server is running. if (!be_roster->IsRunning (kServerSignature)) { - errorCode = be_roster->Launch (kServerSignature); - if (errorCode != B_OK) - goto ErrorExit; - } - + errorCode = be_roster->Launch (kServerSignature); + if (errorCode != B_OK) { + BPath path; + entry_ref ref; + directory_which places[] = {B_COMMON_BIN_DIRECTORY,B_BEOS_BIN_DIRECTORY}; + for (int32 i = 0; i < 2; i++) { + find_directory(places[i],&path); + path.Append("spamfilter"); + if (!BEntry(path.Path()).Exists()) + continue; + get_ref_for_path(path.Path(),&ref); + if ((errorCode = be_roster->Launch (&ref)) == B_OK) + break; + } + if (errorCode != B_OK) + goto ErrorExit; + } + } + + snooze (2000000); + // Set up the messenger to the database server. - serverTeam = be_roster->TeamFor (kServerSignature); - if (serverTeam < 0) - goto ErrorExit; messengerToServer = - BMessenger (kServerSignature, serverTeam, &errorCode); + BMessenger (kServerSignature); if (!messengerToServer.IsValid ()) goto ErrorExit; diff --git a/src/apps/bin/Jamfile b/src/apps/bin/Jamfile index 2eeb132cb3..39d8ba1493 100644 --- a/src/apps/bin/Jamfile +++ b/src/apps/bin/Jamfile @@ -85,6 +85,12 @@ StdBinCommands : be libmail.so ; +# standard commands that need libbe.so, libmail.so, libtracker.so +StdBinCommands + spamfilter.cpp + : be tracker stdc++.r4 libmail.so + ; + SubInclude OBOS_TOP src apps bin addattr ; SubInclude OBOS_TOP src apps bin bash ; SubInclude OBOS_TOP src apps bin bc ; diff --git a/src/apps/bin/spamfilter.cpp b/src/apps/bin/spamfilter.cpp new file mode 100644 index 0000000000..8843bafb71 --- /dev/null +++ b/src/apps/bin/spamfilter.cpp @@ -0,0 +1,7271 @@ +/****************************************************************************** + * $Header: /tmp/bonefish/open-beos/current/src/apps/bin/spamfilter.cpp,v 1.1 2004/11/12 02:55:04 nwhitehorn Exp $ + * + * This is a BeOS program for classifying e-mail messages as spam (unwanted + * junk mail) or as genuine mail using a Bayesian statistical approach. There + * is also a Mail Daemon Replacement add-on to filter mail using the + * classification statistics collected earlier. + * + * See also http://www.paulgraham.com/spam.html for a good writeup and + * http://www.tuxedo.org/~esr/bogofilter/ for another implementation. + * And more recently, Gary Robinson's write up of his improved algorithm + * at http://radio.weblogs.com/0101454/stories/2002/09/16/spamDetection.html + * which gives a better spread in spam ratios and slightly fewer + * misclassifications. + * + * Note that this uses the AGMS coding style, not the OpenTracker one. That + * means no tabs, indents are two spaces, m_ is the prefix for member + * variables, g_ is the prefix for global names, C style comments, constants + * are in all capital letters and most other things are mixed case, it's word + * wrapped to fit in 79 characters per line to make proofreading on paper + * easier, and functions are listed in reverse dependency order so that forward + * declarations (function prototypes with no code) aren't needed. + * + * The Original Design: + * There is a spam database (just a file listing words and number of times they + * were used in spam and non-spam messages) that a BeMailDaemon input filter + * will use when scanning email. It will mark the mail with the spam + * probability (an attribute, optionally a mail header field) and optionally do + * something if the probability exceeds a user defined level (delete message, + * change subject, file in a different folder). Or should that be a different + * filter? Outside the mail system, the probability can be used in queries to + * find spam. + * + * A second user application will be used to update the database. Besides + * showing you the current list of words, you can drag and drop files to mark + * them as spam or non-spam (a balanced binary tree is used internally to make + * word storage fast). It will add a second attribute to the files to show how + * they have been classified by the user (and won't update the database if you + * accidentally try to classify a file again). Besides drag and drop, there + * will be a command line interface and a message passing interface. BeMail + * (or other programs) will then communicate via messages to tell it when the + * user marks a message as spam or not (via having separate delete spam / + * delete genuine mail buttons and a menu item or two). + * + * Plus lots of details, like the rename swap method to update the database + * file (so programs with the old file open aren't affected). A nice tab text + * format so you can open the database in a spreadsheet. Startup and shutdown + * control of the updater from BeMail. Automatic creation of the indices + * needed by the filter. MIME types for the database file. Icons for the app. + * System settings to enable tracker to display the new attributes when viewing + * e-mail (and maybe news articles if someone ever gets around to an NNTP as + * files reader). Documentation. Recursive directory traversal for the + * command line or directory drag and drop. Options for the updater to warn or + * ignore non-email files. Etc. + * + * The Actual Implementation: + * The spam database updates and the test for spam have been combined into one + * program which runs as a server. That way there won't be as long a delay + * when the e-mail system wants to check for spam, because the database is + * already loaded by the server and in memory. The MDR mail filter add-on + * simply sends scripting commands to the server (and starts it up if it isn't + * already running). The filter takes care of marking the messages when it + * gets the rating back from the server, and then the rest of the mail system + * rule chain can delete the message or otherwise manipulate it. + * + * $Log: spamfilter.cpp,v $ + * Revision 1.1 2004/11/12 02:55:04 nwhitehorn + * Added AGMS's excellent spam detection software. Still some weirdness with the configuration interface from E-mail prefs. + * + * Revision 1.87 2004/09/20 15:57:26 nwhitehorn + * Mostly updated the tree to Be/Haiku style identifier naming conventions. I have a few more things to work out, mostly in mail_util.h, and then I'm proceeding to jamify the build system. Then we go into Haiku CVS. + * + * Revision 1.86 2003/07/26 16:47:46 agmsmith + * Bug - wasn't allowing double classification if the user had turned on + * the option to ignore the previous classification. + * + * Revision 1.85 2003/07/08 14:52:57 agmsmith + * Fix bug with classification choices dialog box coming up with weird + * sizes due to RefsReceived message coming in before ReadyToRun had + * finished setting up the default sizes of the controls. + * + * Revision 1.84 2003/07/04 19:59:29 agmsmith + * Now with a GUI option to let you declassify messages (set them back + * to uncertain, rather than spam or genuine). Required a BAlert + * replacement since BAlerts can't do four buttons. + * + * Revision 1.83 2003/07/03 20:40:36 agmsmith + * Added Uncertain option for declassifying messages. + * + * Revision 1.82 2003/06/16 14:57:13 agmsmith + * Detect spam which uses mislabeled text attachments, going by the file name + * extension. + * + * Revision 1.81 2003/04/08 20:27:04 agmsmith + * AGMSBayesianSpamServer now shuts down immediately and returns true if + * it is asked to quit by the registrar. + * + * Revision 1.80 2003/04/07 19:20:27 agmsmith + * Ooops, int64 doesn't exist, use long long instead. + * + * Revision 1.79 2003/04/07 19:05:22 agmsmith + * Now with Allen Brunson's atoll for PPC (you need the %Ld, but that + * becomes %lld on other systems). + * + * Revision 1.78 2003/04/04 22:43:53 agmsmith + * Fixed up atoll PPC processor hack so it would actually work, was just + * returning zero which meant that it wouldn't load in the database file + * (read the size as zero). + * + * Revision 1.77 2003/01/22 03:19:48 agmsmith + * Don't convert words to lower case, the case is important for spam. + * Particularly sentences which start with exciting words, which you + * normally won't use at the start of a sentence (and thus capitalize). + * + * Revision 1.76 2002/12/18 02:29:22 agmsmith + * Add space for the Uncertain display in Tracker. + * + * Revision 1.75 2002/12/18 01:54:37 agmsmith + * Added uncertain sound effect. + * + * Revision 1.74 2002/12/13 23:53:12 agmsmith + * Minimize the window before opening it so that it doesn't flash on the + * screen in server mode. Also load the database when the window is + * displayed so that the user can see the words. + * + * Revision 1.73 2002/12/13 20:55:57 agmsmith + * Documentation. + * + * Revision 1.72 2002/12/13 20:26:11 agmsmith + * Fixed bug with adding messages in strings to database (was limited to + * messages at most 1K long). Also changed default server mode to true + * since that's what people use most. + * + * Revision 1.71 2002/12/11 22:37:30 agmsmith + * Added commands to train on spam and genuine e-mail messages passed + * in string arguments rather then via external files. + * + * Revision 1.70 2002/12/10 22:12:41 agmsmith + * Adding a message to the database now uses a BPositionIO rather than a + * file and file name (for future string rather than file additions). Also + * now re-evaluate a file after reclassifying it so that the user can see + * the new ratio. Also remove the [Spam 99.9%] subject prefix when doing + * a re-evaluation or classification (the number would be wrong). + * + * Revision 1.69 2002/12/10 01:46:04 agmsmith + * Added the Chi-Squared scoring method. + * + * Revision 1.68 2002/11/29 22:08:25 agmsmith + * Change default purge age to 2000 so that hitting the purge button + * doesn't erase stuff from the new sample database. + * + * Revision 1.67 2002/11/25 20:39:39 agmsmith + * Don't need to massage the MIME type since the mail library now does + * the lower case conversion and converts TEXT to text/plain too. + * + * Revision 1.66 2002/11/20 22:57:12 nwhitehorn + * PPC Compatibility Fixes + * + * Revision 1.65 2002/11/10 18:43:55 agmsmith + * Added a time delay to some quitting operations so that scripting commands + * from a second client (like a second e-mail account) will make the program + * abort the quit operation. + * + * Revision 1.64 2002/11/05 18:05:16 agmsmith + * Looked at Nathan's PPC changes (thanks!), modified style a bit. + * + * Revision 1.63 2002/11/04 03:30:22 nwhitehorn + * Now works (or compiles at least) on PowerPC. I'll get around to testing it + * later. + * + * Revision 1.62 2002/11/04 01:03:33 agmsmith + * Fixed warnings so it compiles under the bemaildaemon system. + * + * Revision 1.61 2002/11/03 23:00:37 agmsmith + * Added to the bemaildaemon project on SourceForge. Hmmmm, seems to switch to + * a new version if I commit and specify a message, but doesn't accept the + * message and puts up the text editor. Must be a bug where cvs eats the first + * option after "commit". + * + * Revision 1.60.1.1 2002/10/22 14:29:27 agmsmith + * Needed to recompile with the original Libmail.so from Beta/1 since + * the current library uses a different constructor, and thus wouldn't + * run when used with the old library. + * + * Revision 1.60 2002/10/21 16:41:27 agmsmith + * Return a special error code when no words are found in a message, + * so that messages without text/plain parts can be recognized as + * spam by the mail filter. + * + * Revision 1.59 2002/10/20 21:29:47 agmsmith + * Watch out for MIME types of "text", treat as text/plain. + * + * Revision 1.58 2002/10/20 18:29:07 agmsmith + * *** empty log message *** + * + * Revision 1.57 2002/10/20 18:25:02 agmsmith + * Fix case sensitivity in MIME type tests, and fix text/any test. + * + * Revision 1.56 2002/10/19 17:00:10 agmsmith + * Added the pop-up menu for the tokenize modes. + * + * Revision 1.55 2002/10/19 14:54:06 agmsmith + * Fudge MIME type of body text components so that they get + * treated as text. + * + * Revision 1.54 2002/10/19 00:56:37 agmsmith + * The parsing of e-mail messages seems to be working now, just need + * to add some user interface stuff for the tokenizing mode. + * + * Revision 1.53 2002/10/18 23:37:56 agmsmith + * More mail kit usage, can now decode headers, but more to do. + * + * Revision 1.52 2002/10/16 23:52:33 agmsmith + * Getting ready to add more tokenizing modes, exploring Mail Kit to break + * apart messages into components (and decode BASE64 and other encodings). + * + * Revision 1.51 2002/10/11 20:05:31 agmsmith + * Added installation of sound effect names, which the filter will use. + * + * Revision 1.50 2002/10/02 16:50:02 agmsmith + * Forgot to add credits to the algorithm inventors. + * + * Revision 1.49 2002/10/01 00:39:29 agmsmith + * Added drag and drop to evaluate files or to add them to the list. + * + * Revision 1.48 2002/09/30 19:44:17 agmsmith + * Switched to Gary Robinson's method, removed max spam/genuine word. + * + * Revision 1.47 2002/09/23 17:08:55 agmsmith + * Add an attribute with the spam ratio to files which have been evaluated. + * + * Revision 1.46 2002/09/23 02:50:32 agmsmith + * Fiddling with display width of e-mail attributes. + * + * Revision 1.45 2002/09/23 01:13:56 agmsmith + * Oops, bug in string evaluation scripting. + * + * Revision 1.44 2002/09/22 21:00:55 agmsmith + * Added EvaluateString so that the BeMail add-on can pass the info without + * having to create a temporary file. + * + * Revision 1.43 2002/09/20 19:56:02 agmsmith + * Added about box and button for estimating the spam ratio of a file. + * + * Revision 1.42 2002/09/20 01:22:26 agmsmith + * More testing, decide that an extreme ratio bias point of 0.5 is good. + * + * Revision 1.41 2002/09/19 21:17:12 agmsmith + * Changed a few names and proofread the program. + * + * Revision 1.40 2002/09/19 14:27:17 agmsmith + * Rearranged execution of commands, moving them to a separate looper + * rather than the BApplication, so that thousands of files could be + * processed without worrying about the message queue filling up. + * + * Revision 1.39 2002/09/18 18:47:16 agmsmith + * Stop flickering when the view is partially obscured, update cached + * values in all situations except when app is busy. + * + * Revision 1.38 2002/09/18 18:08:11 agmsmith + * Add a function for evaluating the spam ratio of a message. + * + * Revision 1.37 2002/09/16 01:30:16 agmsmith + * Added Get Oldest command. + * + * Revision 1.36 2002/09/16 00:47:52 agmsmith + * Change the display to counter-weigh the spam ratio by the number of + * messages. + * + * Revision 1.35 2002/09/15 20:49:35 agmsmith + * Scrolling improved, buttons, keys and mouse wheel added. + * + * Revision 1.34 2002/09/15 03:46:10 agmsmith + * Up and down buttons under construction. + * + * Revision 1.33 2002/09/15 02:09:21 agmsmith + * Took out scroll bar. + * + * Revision 1.32 2002/09/15 02:05:30 agmsmith + * Trying to add a scroll bar, but it isn't very useful. + * + * Revision 1.31 2002/09/14 23:06:28 agmsmith + * Now has live updates of the list of words. + * + * Revision 1.30 2002/09/14 19:53:11 agmsmith + * Now with a better display of the words. + * + * Revision 1.29 2002/09/13 21:33:54 agmsmith + * Now draws the words in the word display view, but still primitive. + * + * Revision 1.28 2002/09/13 19:28:02 agmsmith + * Added display of most genuine and most spamiest, fixed up cursor. + * + * Revision 1.27 2002/09/13 03:08:42 agmsmith + * Show current word and message counts, and a busy cursor. + * + * Revision 1.26 2002/09/13 00:00:08 agmsmith + * Fixed up some deadlock problems, now using asynchronous message replies. + * + * Revision 1.25 2002/09/12 17:56:58 agmsmith + * Keep track of words which are spamiest and genuinest. + * + * Revision 1.24 2002/09/12 01:57:10 agmsmith + * Added server mode. + * + * Revision 1.23 2002/09/11 23:30:45 agmsmith + * Added Purge button and ignore classification checkbox. + * + * Revision 1.22 2002/09/11 21:23:13 agmsmith + * Added bulk update choice, purge button, moved to a BView container + * for all the controls (so background colour could be set, and Pulse + * works normally for it too). + * + * Revision 1.21 2002/09/10 22:52:49 agmsmith + * You can now change the database name in the GUI. + * + * Revision 1.20 2002/09/09 14:20:42 agmsmith + * Now can have multiple backups, and implemented refs received. + * + * Revision 1.19 2002/09/07 19:14:56 agmsmith + * Added standard GUI measurement code. + * + * Revision 1.18 2002/09/06 21:03:03 agmsmith + * Rearranging code to avoid forward references when adding a window class. + * + * Revision 1.17 2002/09/06 02:54:00 agmsmith + * Added the ability to purge old words from the database. + * + * Revision 1.16 2002/09/05 00:46:03 agmsmith + * Now adds spam to the database! + * + * Revision 1.15 2002/09/04 20:32:15 agmsmith + * Read ahead a couple of letters to decode quoted-printable better. + * + * Revision 1.14 2002/09/04 03:10:03 agmsmith + * Can now tokenize (break into words) a text file. + * + * Revision 1.13 2002/09/03 21:50:54 agmsmith + * Count database command, set up MIME type for the database file. + * + * Revision 1.12 2002/09/03 19:55:54 agmsmith + * Added loading and saving the database. + * + * Revision 1.11 2002/09/02 03:35:33 agmsmith + * Create indices and set up attribute associations with the e-mail MIME type. + * + * Revision 1.10 2002/09/01 15:52:49 agmsmith + * Can now delete the database. + * + * Revision 1.9 2002/08/31 21:55:32 agmsmith + * Yet more scripting. + * + * Revision 1.8 2002/08/31 21:41:37 agmsmith + * Under construction, with example code to decode a B_REPLY. + * + * Revision 1.7 2002/08/30 19:29:06 agmsmith + * Combined loading and saving settings into one function. + * + * Revision 1.6 2002/08/30 02:01:10 agmsmith + * Working on loading and saving settings. + * + * Revision 1.5 2002/08/29 23:17:42 agmsmith + * More scripting. + * + * Revision 1.4 2002/08/28 00:40:52 agmsmith + * Scripting now seems to work, at least the messages flow properly. + * + * Revision 1.3 2002/08/25 21:51:44 agmsmith + * Getting the about text formatting right. + * + * Revision 1.2 2002/08/25 21:28:20 agmsmith + * Trying out the BeOS scripting system as a way of implementing the program. + * + * Revision 1.1 2002/08/24 02:27:51 agmsmith + * Initial revision + */ + +/* Standard C Library. */ + +#include +#include +#include + +#if __POWERPC__ /* atoll is missing from the PowerPC standard library. */ +static long long atoll (const char *str) { + long long result = 0; + if (!str || !str[0]) + return 0; + if (sscanf (str, "%Ld", &result) != 1) + result = 0; + return result; +} +#endif + +/* Standard C++ library. */ + +#include + +/* STL (Standard Template Library) headers. */ + +#include +#include +#include +#include +#include + +/* BeOS (Be Operating System) headers. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Included from the Mail Daemon Replacement project (MDR) include/public +directory, available from http://sourceforge.net/projects/bemaildaemon/ */ + +#include +#include + + +/****************************************************************************** + * Global variables, and not-so-variable things too. Grouped by functionality. + */ + +static float g_MarginBetweenControls; /* Space of a letter "M" between them. */ +static float g_LineOfTextHeight; /* Height of text the current font. */ +static float g_StringViewHeight; /* Height of a string view text box. */ +static float g_ButtonHeight; /* How many pixels tall buttons are. */ +static float g_CheckBoxHeight; /* Same for check boxes. */ +static float g_RadioButtonHeight; /* Also for radio buttons. */ +static float g_PopUpMenuHeight; /* Again for pop-up menus. */ +static float g_TextBoxHeight; /* Ditto for editable text controls. */ + +static const char *g_ABSAppSignature = + "application/x-vnd.agmsmith.AGMSBayesianSpamServer"; + +static const char *g_ABSDatabaseFileMIMEType = + "text/x-vnd.agmsmith.spam_probability_database"; + +static const char *g_DefaultDatabaseFileName = + "AGMSBayesianSpam Database"; + +static const char *g_DatabaseRecognitionString = + "Spam Database File"; + +static const char *g_AttributeNameClassification = "MAIL:classification"; +static const char *g_AttributeNameSpamRatio = "MAIL:ratio_spam"; +static const char *g_BeepGenuine = "AGMSBayes-Genuine"; +static const char *g_BeepSpam = "AGMSBayes-Spam"; +static const char *g_BeepUncertain = "AGMSBayes-Uncertain"; +static const char *g_ClassifiedSpam = "Spam"; +static const char *g_ClassifiedGenuine = "Genuine"; +static const char *g_DataName = "data"; +static const char *g_ResultName = "result"; + +static const char *g_SettingsDirectoryName = "AGMSBayesianSpam"; +static const char *g_SettingsFileName = "AGMSBayesianSpamServer Settings"; +static const uint32 g_SettingsWhatCode = 'ABSS'; +static const char *g_BackupSuffix = ".backup %d"; +static const int g_MaxBackups = 10; /* Numbered from 0 to g_MaxBackups - 1. */ +static const size_t g_MaxWordLength = 50; /* Words longer than this aren't. */ +static const int g_MaxInterestingWords = 150; /* Top N words are examined. */ +static const double g_RobinsonS = 0.45; /* Default weight for no data. */ +static const double g_RobinsonX = 0.5; /* Halfway point for no data. */ + +static bool g_CommandLineMode; + /* TRUE if the program was started from the command line (and thus should + exit after processing the command), FALSE if it is running with a graphical + user interface. */ + +static bool g_ServerMode; + /* When TRUE the program runs in server mode - error messages don't result in + pop-up dialog boxes, but you can still see them in stderr. Also the window + is minimized, if it exists. */ + +static int g_QuitCountdown = -1; + /* Set to the number of pulse timing events (about one every half second) to + count down before the program quits. Negative means stop counting. Zero + means quit at the next pulse event. This is used to keep the program alive + for a short while after someone requests that it quit, in case more scripting + commands come in, which will stop the countdown. Needed to handle the case + where there are multiple e-mail accounts all requesting spam identification, + and one finishes first and tells the server to quit. It also checks to see + that there is no more work to do before trying to quit. */ + +static volatile bool g_AppReadyToRunCompleted = false; + /* The BApplication starts processing messages before ReadyToRun finishes, + which can lead to initialisation problems (button heights not determined). + So wait for this to turn TRUE in code that might run early, like + RefsReceived. */ + +static class CommanderLooper *g_CommanderLooperPntr = NULL; +static BMessenger *g_CommanderMessenger = NULL; + /* Some globals for use with the looper which processes external commands + (arguments received, file references received), needed for avoiding deadlocks + which would happen if the BApplication sent a scripting message to itself. */ + +typedef enum PropertyNumbersEnum +{ + PN_DATABASE_FILE = 0, + PN_SPAM, + PN_SPAM_STRING, + PN_GENUINE, + PN_GENUINE_STRING, + PN_UNCERTAIN, + PN_IGNORE_PREVIOUS_CLASSIFICATION, + PN_SERVER_MODE, + PN_FLUSH, + PN_PURGE_AGE, + PN_PURGE_POPULARITY, + PN_PURGE, + PN_OLDEST, + PN_EVALUATE, + PN_EVALUATE_STRING, + PN_RESET_TO_DEFAULTS, + PN_INSTALL_THINGS, + PN_TOKENIZE_MODE, + PN_SCORING_MODE, + PN_MAX +} PropertyNumbers; + +static char * g_PropertyNames [PN_MAX] = +{ + "DatabaseFile", + "Spam", + "SpamString", + "Genuine", + "GenuineString", + "Uncertain", + "IgnorePreviousClassification", + "ServerMode", + "Flush", + "PurgeAge", + "PurgePopularity", + "Purge", + "Oldest", + "Evaluate", + "EvaluateString", + "ResetToDefaults", + "InstallThings", + "TokenizeMode", + "ScoringMode" +}; + +/* This array lists the scripting commands we can handle, in a format that the +scripting system can understand too. */ + +static struct property_info g_ScriptingPropertyList [] = +{ + /* *name; commands[10]; specifiers[10]; *usage; extra_data; ... */ + {g_PropertyNames[PN_DATABASE_FILE], {B_GET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Get the pathname of the current database file. " + "The default name is something like B_USER_SETTINGS_DIRECTORY / " + "AGMSBayesianSpam / AGMSBayesianSpamServer Database", PN_DATABASE_FILE, + {}, {}, {}}, + {g_PropertyNames[PN_DATABASE_FILE], {B_SET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Change the pathname of the database file to " + "use. It will automatically be converted to an absolute path name, " + "so make sure the parent directories exist before setting it. If it " + "doesn't exist, you'll have to use the create command next.", + PN_DATABASE_FILE, {}, {}, {}}, + {g_PropertyNames[PN_DATABASE_FILE], {B_CREATE_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Creates a new empty database, will replace " + "the existing database file too.", PN_DATABASE_FILE, {}, {}, {}}, + {g_PropertyNames[PN_DATABASE_FILE], {B_DELETE_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Deletes the database file and all backup copies " + "of that file too. Really only of use for uninstallers.", + PN_DATABASE_FILE, {}, {}, {}}, + {g_PropertyNames[PN_DATABASE_FILE], {B_COUNT_PROPERTIES, 0}, + {B_DIRECT_SPECIFIER, 0}, "Returns the number of words in the database.", + PN_DATABASE_FILE, {}, {}, {}}, + {g_PropertyNames[PN_SPAM], {B_SET_PROPERTY, 0}, {B_DIRECT_SPECIFIER, 0}, + "Adds the spam in the given file (specify full pathname to be safe) to " + "the database. The words in the files will be added to the list of words " + "in the database that identify spam messages. The files processed will " + "also have the attribute MAIL:classification added with a value of " + "\"Spam\" or \"Genuine\" as specified. They also have their spam ratio " + "attribute updated, as if you had also used the Evaluate command on " + "them. If they already have the MAIL:classification " + "attribute and it matches the new classification then they won't get " + "processed (and if it is different, they will get removed from the " + "statistics for the old class and added to the statistics for the new " + "one). You can turn off that behaviour with the " + "IgnorePreviousClassification property. The command line version lets " + "you specify more than one pathname.", PN_SPAM, {}, {}, {}}, + {g_PropertyNames[PN_SPAM], {B_COUNT_PROPERTIES, 0}, {B_DIRECT_SPECIFIER, 0}, + "Returns the number of spam messages in the database.", PN_SPAM, + {}, {}, {}}, + {g_PropertyNames[PN_SPAM_STRING], {B_SET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Adds the spam in the given string (assumed to " + "be the text of a whole e-mail message, not just a file name) to the " + "database.", PN_SPAM_STRING, {}, {}, {}}, + {g_PropertyNames[PN_GENUINE], {B_SET_PROPERTY, 0}, {B_DIRECT_SPECIFIER, 0}, + "Similar to adding spam except that the message file is added to the " + "genuine statistics.", PN_GENUINE, {}, {}, {}}, + {g_PropertyNames[PN_GENUINE], {B_COUNT_PROPERTIES, 0}, + {B_DIRECT_SPECIFIER, 0}, "Returns the number of genuine messages in the " + "database.", PN_GENUINE, {}, {}, {}}, + {g_PropertyNames[PN_GENUINE_STRING], {B_SET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Adds the genuine message in the given string " + "(assumed to be the text of a whole e-mail message, not just a file name) " + "to the database.", PN_GENUINE_STRING, {}, {}, {}}, + {g_PropertyNames[PN_UNCERTAIN], {B_SET_PROPERTY, 0}, {B_DIRECT_SPECIFIER, 0}, + "Similar to adding spam except that the message file is removed from the " + "database, undoing the previous classification. Obviously, it needs to " + "have been classified previously (using the file attributes) so it can " + "tell if it is removing spam or genuine words.", PN_UNCERTAIN, {}, {}, {}}, + {g_PropertyNames[PN_IGNORE_PREVIOUS_CLASSIFICATION], {B_SET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "If set to true then the previous classification " + "(which was saved as an attribute of the e-mail message file) will be " + "ignored, so that you can add the message to the database again. If set " + "to false (the normal case), the attribute will be examined, and if the " + "message has already been classified as what you claim it is, nothing " + "will be done. If it was misclassified, then the message will be removed " + "from the statistics for the old class and added to the stats for the " + "new classification you have requested.", + PN_IGNORE_PREVIOUS_CLASSIFICATION, {}, {}, {}}, + {g_PropertyNames[PN_IGNORE_PREVIOUS_CLASSIFICATION], {B_GET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Find out the current setting of the flag for " + "ignoring the previously recorded classification.", + PN_IGNORE_PREVIOUS_CLASSIFICATION, {}, {}, {}}, + {g_PropertyNames[PN_SERVER_MODE], {B_SET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "If set to true then error messages get printed " + "to the standard error stream rather than showing up in an alert box. " + "It also starts up with the window minimized.", PN_SERVER_MODE, + {}, {}, {}}, + {g_PropertyNames[PN_SERVER_MODE], {B_GET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Find out the setting of the server mode flag.", + PN_SERVER_MODE, {}, {}, {}}, + {g_PropertyNames[PN_FLUSH], {B_EXECUTE_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Writes out the database file to disk, if it has " + "been updated in memory but hasn't been saved to disk. It will " + "automatically get written when the program exits, so this command is " + "mostly useful for server mode.", PN_FLUSH, {}, {}, {}}, + {g_PropertyNames[PN_PURGE_AGE], {B_SET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Sets the old age limit. Words which haven't " + "been updated since this many message additions to the database may be " + "deleted when you do a purge. A good value is 1000, meaning that if a " + "word hasn't appeared in the last 1000 spam/genuine messages, it will " + "be forgotten. Zero will purge all words, 1 will purge words not in " + "the last message added to the database, 2 will purge words not in the " + "last two messages added, and so on. This is mostly useful for " + "removing those one time words which are often hunks of binary garbage, " + "not real words. This acts in combination with the popularity limit; " + "both conditions have to be valid before the word gets deleted.", + PN_PURGE_AGE, {}, {}, {}}, + {g_PropertyNames[PN_PURGE_AGE], {B_GET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Gets the old age limit.", PN_PURGE_AGE, + {}, {}, {}}, + {g_PropertyNames[PN_PURGE_POPULARITY], {B_SET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Sets the popularity limit. Words which aren't " + "this popular may be deleted when you do a purge. A good value is 5, " + "which means that the word is safe from purging if it has been seen in 6 " + "or more e-mail messages. If it's only in 5 or less, then it may get " + "purged. The extreme is zero, where only words that haven't been seen " + "in any message are deleted (usually means no words). This acts in " + "combination with the old age limit; both conditions have to be valid " + "before the word gets deleted.", PN_PURGE_POPULARITY, {}, {}, {}}, + {g_PropertyNames[PN_PURGE_POPULARITY], {B_GET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Gets the purge popularity limit.", + PN_PURGE_POPULARITY, {}, {}, {}}, + {g_PropertyNames[PN_PURGE], {B_EXECUTE_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Purges the old obsolete words from the " + "database, if they are old enough according to the age limit and also " + "unpopular enough according to the popularity limit.", PN_PURGE, + {}, {}, {}}, + {g_PropertyNames[PN_OLDEST], {B_GET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Gets the age of the oldest message in the " + "database. It's relative to the beginning of time, so you need to do " + "(total messages - age - 1) to see how many messages ago it was added.", + PN_OLDEST, {}, {}, {}}, + {g_PropertyNames[PN_EVALUATE], {B_SET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Evaluates a given file (by path name) to see " + "if it is spam or not. Returns the ratio of spam probability vs genuine " + "probability, 0.0 meaning completely genuine, 1.0 for completely spam. " + "Normally you should safely be able to consider it as spam if it is over " + "0.56 for the Robinson scoring method. For the ChiSquared method, the " + "numbers are near 0 for genuine, near 1 for spam, and anywhere in the " + "middle means it can't decide. The program attaches a MAIL:ratio_spam " + "attribute with the ratio as its " + "float32 value to the file. Also returns the top few interesting words " + "in \"words\" and the associated per-word probability ratios in " + "\"ratios\".", PN_EVALUATE, {}, {}, {}}, + {g_PropertyNames[PN_EVALUATE_STRING], {B_SET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Like Evaluate, but rather than a file name, " + "the string argument contains the entire text of the message to be " + "evaluated.", PN_EVALUATE_STRING, {}, {}, {}}, + {g_PropertyNames[PN_RESET_TO_DEFAULTS], {B_EXECUTE_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Resets all the configuration options to the " + "default values, including the database name.", PN_RESET_TO_DEFAULTS, + {}, {}, {}}, + {g_PropertyNames[PN_INSTALL_THINGS], {B_EXECUTE_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Creates indices for the MAIL:classification and " + "MAIL:ratio_spam attributes on all volumes which support BeOS queries, " + "identifies them to the system as e-mail related attributes (modifies " + "the text/x-email MIME type), and sets up the new MIME type " + "(text/x-vnd.agmsmith.spam_probability_database) for the database file. " + "Also registers names for the sound effects used by the separate filter " + "program (use the installsound BeOS program or the Sounds preferences " + "program to associate sound files with the names).", PN_INSTALL_THINGS, + {}, {}, {}}, + {g_PropertyNames[PN_TOKENIZE_MODE], {B_SET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Sets the method used for breaking up the " + "message into words. Use \"Whole\" for the whole file (also use it for " + "non-email files). The file isn't broken into parts; the whole thing is " + "converted into words, headers and attachments are just more raw data. " + "Well, not quite raw data since it converts quoted-printable codes " + "(equals sign followed by hex digits or end of line) to the equivalent " + "single characters. \"PlainText\" breaks the file into MIME components " + "and only looks at the ones which are of MIME type text/plain. " + "\"AnyText\" will look for words in all text/* things, including " + "text/html attachments. \"AllParts\" will decode all message components " + "and look for words in them, including binary attachments. " + "\"JustHeader\" will only look for words in the message header. " + "\"AllPartsAndHeader\", \"PlainTextAndHeader\" and \"AnyTextAndHeader\" " + "will also include the words from the message headers.", PN_TOKENIZE_MODE, + {}, {}, {}}, + {g_PropertyNames[PN_TOKENIZE_MODE], {B_GET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Gets the method used for breaking up the " + "message into words.", PN_TOKENIZE_MODE, {}, {}, {}}, + {g_PropertyNames[PN_SCORING_MODE], {B_SET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Sets the method used for combining the " + "probabilities of individual words into an overall score. " + "\"Robinson\" mode will use Gary Robinson's nth root of the product " + "method. It gives a nice range of values between 0 and 1 so you can " + "see shades of spaminess. The cutoff point between spam and genuine " + "varies depending on your database of words (0.56 was one point in " + "some experiments). \"ChiSquared\" mode will use chi-squared " + "statistics to evaluate the difference in probabilities that the lists " + "of word ratios are random. The result is very close to 0 for genuine " + "and very close to 1 for spam, and near the middle if it is uncertain.", + PN_SCORING_MODE, {}, {}, {}}, + {g_PropertyNames[PN_SCORING_MODE], {B_GET_PROPERTY, 0}, + {B_DIRECT_SPECIFIER, 0}, "Gets the method used for combining the " + "individual word ratios into an overall score.", PN_SCORING_MODE, + {}, {}, {}}, + {0, {0}, {0}, 0, 0, {}, {}, {}} /* End of list of property commands. */ +}; + + +/* The various scoring modes as text and enums. See PN_SCORING_MODE. */ + +typedef enum ScoringModeEnum +{ + SM_ROBINSON = 0, + SM_CHISQUARED, + SM_MAX +} ScoringModes; + +static char * g_ScoringModeNames [SM_MAX] = +{ + "Robinson", + "ChiSquared" +}; + + +/* The various tokenizing modes as text and enums. See PN_TOKENIZE_MODE. */ + +typedef enum TokenizeModeEnum +{ + TM_WHOLE = 0, + TM_PLAIN_TEXT, + TM_PLAIN_TEXT_HEADER, + TM_ANY_TEXT, + TM_ANY_TEXT_HEADER, + TM_ALL_PARTS, + TM_ALL_PARTS_HEADER, + TM_JUST_HEADER, + TM_MAX +} TokenizeModes; + +static char * g_TokenizeModeNames [TM_MAX] = +{ + "Whole", + "PlainText", + "PlainTextAndHeader", + "AnyText", + "AnyTextAndHeader", + "AllParts", + "AllPartsAndHeader", + "JustHeader" +}; + + +/* Possible message classifications. */ + +typedef enum ClassificationTypesEnum +{ + CL_GENUINE = 0, + CL_SPAM, + CL_UNCERTAIN, + CL_MAX +} ClassificationTypes; + +static const char * g_ClassificationTypeNames [CL_MAX] = +{ + g_ClassifiedGenuine, + g_ClassifiedSpam, + "Uncertain" +}; + + +/* Some polygon graphics for the scroll arrows. */ + +static BPoint g_UpLinePoints [] = +{ + BPoint (8, 2 * (1)), + BPoint (14, 2 * (6)), + BPoint (10, 2 * (6)), + BPoint (10, 2 * (13)), + BPoint (6, 2 * (13)), + BPoint (6, 2 * (6)), + BPoint (2, 2 * (6)) +}; + +static BPoint g_DownLinePoints [] = +{ + BPoint (8, 2 * (14-1)), + BPoint (14, 2 * (14-6)), + BPoint (10, 2 * (14-6)), + BPoint (10, 2 * (14-13)), + BPoint (6, 2 * (14-13)), + BPoint (6, 2 * (14-6)), + BPoint (2, 2 * (14-6)) +}; + +static BPoint g_UpPagePoints [] = +{ + BPoint (8, 2 * (1)), + BPoint (13, 2 * (6)), + BPoint (10, 2 * (6)), + BPoint (14, 2 * (10)), + BPoint (10, 2 * (10)), + BPoint (10, 2 * (13)), + BPoint (6, 2 * (13)), + BPoint (6, 2 * (10)), + BPoint (2, 2 * (10)), + BPoint (6, 2 * (6)), + BPoint (3, 2 * (6)) +}; + +static BPoint g_DownPagePoints [] = +{ + BPoint (8, 2 * (14-1)), + BPoint (13, 2 * (14-6)), + BPoint (10, 2 * (14-6)), + BPoint (14, 2 * (14-10)), + BPoint (10, 2 * (14-10)), + BPoint (10, 2 * (14-13)), + BPoint (6, 2 * (14-13)), + BPoint (6, 2 * (14-10)), + BPoint (2, 2 * (14-10)), + BPoint (6, 2 * (14-6)), + BPoint (3, 2 * (14-6)) +}; + + + +/****************************************************************************** + * Each word in the spam database gets one of these structures. The database + * has a string (the word) as the key and this structure as the value + * (statistics for that word). + */ + +typedef struct StatisticsStruct +{ + uint32 age; + /* Sequence number for the time when this word was last updated in the + database, so that we can remove old words (haven't been seen in recent + spam). It's zero for the first file ever added (spam or genuine) to the + database, 1 for all words added or updated by the second file, etc. If a + later file updates an existing word, it gets the age of the later file. */ + + uint32 genuineCount; + /* Number of genuine messages that have this word. */ + + uint32 spamCount; + /* A count of the number of spam e-mail messages which contain the word. */ + +} StatisticsRecord, *StatisticsPointer; + +typedef map StatisticsMap; + /* Define this type which will be used for our main data storage facility, so + we can more conveniently specify things that are derived from it, like + iterators. */ + + + +/****************************************************************************** + * An alert box asking how the user wants to mark messages. There are buttons + * for each classification category, and a checkbox to mark all remaining N + * messages the same way. And a cancel button. To use it, first create the + * ClassificationChoicesWindow, specifying the input arguments. Then call the + * Go method which will show the window, stuff the user's answer into your + * output arguments (class set to CL_MAX if the user cancels), and destroy the + * window. Implemented because BAlert only allows 3 buttons, max! + */ + +class ClassificationChoicesWindow : public BWindow +{ +public: + /* Constructor and destructor. */ + ClassificationChoicesWindow (BRect FrameRect, + const char *FileName, int NumberOfFiles); + + /* BeOS virtual functions. */ + virtual void MessageReceived (BMessage *MessagePntr); + + /* Our methods. */ + void Go (bool *BulkModeSelectedPntr, + ClassificationTypes *ChoosenClassificationPntr); + + /* Various message codes for various buttons etc. */ + static const uint32 MSG_CLASS_BUTTONS = 'ClB0'; + static const uint32 MSG_CANCEL_BUTTON = 'Cncl'; + static const uint32 MSG_BULK_CHECKBOX = 'BlkK'; + +private: + /* Member variables. */ + bool *m_BulkModeSelectedPntr; + ClassificationTypes *m_ChoosenClassificationPntr; +}; + +class ClassificationChoicesView : public BView +{ +public: + /* Constructor and destructor. */ + ClassificationChoicesView (BRect FrameRect, + const char *FileName, int NumberOfFiles); + + /* BeOS virtual functions. */ + virtual void AttachedToWindow (); + virtual void GetPreferredSize (float *width, float *height); + +private: + /* Member variables. */ + const char *m_FileName; + int m_NumberOfFiles; + float m_PreferredBottomY; +}; + + + +/****************************************************************************** + * Due to deadlock problems with the BApplication posting scripting messages to + * itself, we need to add a second Looper. Its job is to just to convert + * command line arguments and arguments from the Tracker (refs received) into a + * series of scripting commands sent to the main BApplication. It also prints + * out the replies received (to stdout for command line replies). An instance + * of this class will be created and run by the main() function, and shut down + * by it too. + */ + +class CommanderLooper : public BLooper +{ +public: + CommanderLooper (); + ~CommanderLooper (); + virtual void MessageReceived (BMessage *MessagePntr); + + void CommandArguments (int argc, char **argv); + void CommandReferences (BMessage *MessagePntr, + bool BulkMode = false, + ClassificationTypes BulkClassification = CL_GENUINE); + bool IsBusy (); + +private: + void ProcessArgs (BMessage *MessagePntr); + void ProcessRefs (BMessage *MessagePntr); + + static const uint32 MSG_COMMAND_ARGUMENTS = 'CArg'; + static const uint32 MSG_COMMAND_FILE_REFS = 'CRef'; + + bool m_IsBusy; +}; + + + +/****************************************************************************** + * This view contains the various buttons and other controls for setting + * configuration options and displaying the state of the database (but not the + * actual list of words). It will appear in the top half of the + * DatabaseWindow. + */ + +class ControlsView : public BView +{ +public: + /* Constructor and destructor. */ + ControlsView (BRect NewBounds); + ~ControlsView (); + + /* BeOS virtual functions. */ + virtual void AttachedToWindow (); + virtual void FrameResized (float Width, float Height); + virtual void MessageReceived (BMessage *MessagePntr); + virtual void Pulse (); + +private: + /* Various message codes for various buttons etc. */ + static const uint32 MSG_BROWSE_BUTTON = 'Brws'; + static const uint32 MSG_DATABASE_NAME = 'DbNm'; + static const uint32 MSG_ESTIMATE_BUTTON = 'Estm'; + static const uint32 MSG_ESTIMATE_FILE_REFS = 'ERef'; + static const uint32 MSG_IGNORE_CLASSIFICATION = 'IPCl'; + static const uint32 MSG_PURGE_AGE = 'PuAg'; + static const uint32 MSG_PURGE_BUTTON = 'Purg'; + static const uint32 MSG_PURGE_POPULARITY = 'PuPo'; + static const uint32 MSG_SERVER_MODE = 'SrvM'; + + /* Our member functions. */ + void BrowseForDatabaseFile (); + void BrowseForFileToEstimate (); + void PollServerForChanges (); + + /* Member variables. */ + BButton *m_AboutButtonPntr; + BButton *m_AddExampleButtonPntr; + BButton *m_BrowseButtonPntr; + BFilePanel *m_BrowseFilePanelPntr; + BButton *m_CreateDatabaseButtonPntr; + char m_DatabaseFileNameCachedValue [PATH_MAX]; + BTextControl *m_DatabaseFileNameTextboxPntr; + bool m_DatabaseLoadDone; + BButton *m_EstimateSpamButtonPntr; + BFilePanel *m_EstimateSpamFilePanelPntr; + uint32 m_GenuineCountCachedValue; + BTextControl *m_GenuineCountTextboxPntr; + bool m_IgnorePreviousClassCachedValue; + BCheckBox *m_IgnorePreviousClassCheckboxPntr; + BButton *m_InstallThingsButtonPntr; + uint32 m_PurgeAgeCachedValue; + BTextControl *m_PurgeAgeTextboxPntr; + BButton *m_PurgeButtonPntr; + uint32 m_PurgePopularityCachedValue; + BTextControl *m_PurgePopularityTextboxPntr; + BButton *m_ResetToDefaultsButtonPntr; + ScoringModes m_ScoringModeCachedValue; + BMenuBar *m_ScoringModeMenuBarPntr; + BPopUpMenu *m_ScoringModePopUpMenuPntr; + bool m_ServerModeCachedValue; + BCheckBox *m_ServerModeCheckboxPntr; + uint32 m_SpamCountCachedValue; + BTextControl *m_SpamCountTextboxPntr; + bigtime_t m_TimeOfLastPoll; + TokenizeModes m_TokenizeModeCachedValue; + BMenuBar *m_TokenizeModeMenuBarPntr; + BPopUpMenu *m_TokenizeModePopUpMenuPntr; + uint32 m_WordCountCachedValue; + BTextControl *m_WordCountTextboxPntr; +}; + + + +/****************************************************************************** + * This view contains the list of words. It displays as many as can fit in the + * view rectangle, starting at a specified word (so it can simulate scrolling). + * Usually it will appear in the bottom half of the DatabaseWindow. + */ + +class WordsView : public BView +{ +public: + /* Constructor and destructor. */ + WordsView (BRect NewBounds); + + /* BeOS virtual functions. */ + virtual void AttachedToWindow (); + virtual void Draw (BRect UpdateRect); + virtual void KeyDown (const char *BufferPntr, int32 NumBytes); + virtual void MakeFocus (bool Focused); + virtual void MessageReceived (BMessage *MessagePntr); + virtual void MouseDown (BPoint point); + virtual void Pulse (); + +private: + /* Various message codes for various buttons etc. */ + static const uint32 MSG_LINE_DOWN = 'LnDn'; + static const uint32 MSG_LINE_UP = 'LnUp'; + static const uint32 MSG_PAGE_DOWN = 'PgDn'; + static const uint32 MSG_PAGE_UP = 'PgUp'; + + /* Our member functions. */ + void MoveTextUpOrDown (uint32 MovementType); + void RefsDroppedHere (BMessage *MessagePntr); + + /* Member variables. */ + BPictureButton *m_ArrowLineDownPntr; + BPictureButton *m_ArrowLineUpPntr; + BPictureButton *m_ArrowPageDownPntr; + BPictureButton *m_ArrowPageUpPntr; + /* Various buttons for controlling scrolling, since we can't use a scroll + bar. To make them less obvious, their background view colour needs to be + changed whenever the main view's colour changes. */ + + float m_AscentHeight; + /* The ascent height for the font used to draw words. Height from the top + of the highest letter to the base line (which is near the middle bottom of + the letters, the line where you would align your writing of the text by + hand, all letters have part above, some also have descenders below this + line). */ + + rgb_color m_BackgroundColour; + /* The current background colour. Changes when the focus changes. */ + + uint32 m_CachedTotalGenuineMessages; + uint32 m_CachedTotalSpamMessages; + uint32 m_CachedWordCount; + /* These are cached copies of the similar values in the BApplication. They + reflect what's currently displayed. If they are different than the values + from the BApplication then the polling loop will try to redraw the display. + They get set to the values actually used during drawing when drawing is + successful. */ + + char m_FirstDisplayedWord [g_MaxWordLength + 1]; + /* The scrolling display starts at this word. Since we can't use index + numbers (word[12345] for example), we use the word itself. The scroll + buttons set this to the next or previous word in the database. Typing by + the user when the view has the focus will also change this starting word. + */ + + rgb_color m_FocusedColour; + /* The colour to use for focused mode (typing by the user is received by + our view). */ + + bigtime_t m_LastTimeAKeyWasPressed; + /* Records the time when a key was last pressed. Used for determining when + the user has stopped typing a batch of letters. */ + + float m_LineHeight; + /* Height of a line of text in the font used for the word display. + Includes the height of the letters plus a bit of extra space for between + the lines (called leading). */ + + BFont m_TextFont; + /* The font used to draw the text in the window. */ + + float m_TextHeight; + /* Maximum total height of the letters in the text, includes the part above + the baseline and the part below. Doesn't include the sliver of space + between lines. */ + + rgb_color m_UnfocusedColour; + /* The colour to use for unfocused mode, when user typing isn't active. */ +}; + + + +/****************************************************************************** + * The BWindow class for this program. It displays the database in real time, + * and has various buttons and gadgets in the top half for changing settings + * (live changes, no OK button, and they reflect changes done by other programs + * using the server too). The bottom half is a scrolling view listing all the + * words in the database. A simple graphic blotch behind each word shows + * whether the word is strongly or weakly related to spam or genuine messages. + * Most operations go through the scripting message system, but it also peeks + * at the BApplication data for examining simple things and when redrawing the + * list of words. + */ + +class DatabaseWindow : public BWindow +{ +public: + /* Constructor and destructor. */ + DatabaseWindow (); + + /* BeOS virtual functions. */ + virtual void MessageReceived (BMessage *MessagePntr); + virtual bool QuitRequested (); + +private: + /* Member variables. */ + ControlsView *m_ControlsViewPntr; + WordsView *m_WordsViewPntr; +}; + + + +/****************************************************************************** + * ABSApp is the BApplication class for this program. This handles messages + * from the outside world (requests to load a database, or to add files to the + * collection). It responds to command line arguments (if you start up the + * program a second time, the system will just send the arguments to the + * existing running program). It responds to scripting messages. And it + * responds to messages from the window. Its thread does the main work of + * updating the database and reading / writing files. + */ + +class ABSApp : public BApplication +{ +public: + /* Constructor and destructor. */ + ABSApp (); + ~ABSApp (); + + /* BeOS virtual functions. */ + virtual void AboutRequested (); + virtual void ArgvReceived (int32 argc, char **argv); + virtual status_t GetSupportedSuites (BMessage *MessagePntr); + virtual void MessageReceived (BMessage *MessagePntr); + virtual void Pulse (); + virtual bool QuitRequested (); + virtual void ReadyToRun (); + virtual void RefsReceived (BMessage *MessagePntr); + virtual BHandler *ResolveSpecifier (BMessage *MessagePntr, int32 Index, + BMessage *SpecifierMsgPntr, int32 SpecificationKind, const char *Property); + +private: + /* Our member functions. */ + status_t AddFileToDatabase (ClassificationTypes IsSpamOrWhat, + const char *FileName, char *ErrorMessage); + status_t AddPositionIOToDatabase (ClassificationTypes IsSpamOrWhat, + BPositionIO *MessageIOPntr, const char *OptionalFileName, + char *ErrorMessage); + status_t AddStringToDatabase (ClassificationTypes IsSpamOrWhat, + const char *String, char *ErrorMessage); + void AddWordsToSet (const char *InputString, + size_t NumberOfBytes, set &WordSet); + status_t CreateDatabaseFile (char *ErrorMessage); + void DefaultSettings (); + status_t DeleteDatabaseFile (char *ErrorMessage); + status_t EvaluateFile (const char *PathName, BMessage *ReplyMessagePntr, + char *ErrorMessage); + status_t EvaluatePositionIO (BPositionIO *PositionIOPntr, + const char *OptionalFileName, BMessage *ReplyMessagePntr, + char *ErrorMessage); + status_t EvaluateString (const char *BufferPntr, ssize_t BufferSize, + BMessage *ReplyMessagePntr, char *ErrorMessage); + status_t GetWordsFromPositionIO (BPositionIO *PositionIOPntr, + const char *OptionalFileName, set &WordSet, char *ErrorMessage); + status_t InstallThings (char *ErrorMessage); + status_t LoadDatabaseIfNeeded (char *ErrorMessage); + status_t LoadSaveDatabase (bool DoLoad, char *ErrorMessage); +public: + status_t LoadSaveSettings (bool DoLoad); +private: + status_t MakeBackup (char *ErrorMessage); + void MakeDatabaseEmpty (); + void ProcessScriptingMessage (BMessage *MessagePntr, + struct property_info *PropInfoPntr); + status_t PurgeOldWords (char *ErrorMessage); + status_t RecursivelyTokenizeMailComponent ( + BMailComponent *ComponentPntr, const char *OptionalFileName, + set &WordSet, char *ErrorMessage, int RecursionLevel); + status_t SaveDatabaseIfNeeded (char *ErrorMessage); + status_t TokenizeParts (BPositionIO *PositionIOPntr, + const char *OptionalFileName, set &WordSet, char *ErrorMessage); + status_t TokenizeWhole (BPositionIO *PositionIOPntr, + const char *OptionalFileName, set &WordSet, char *ErrorMessage); + +public: + /* Member variables. Many are read by the window thread to see if it needs + updating, and to draw the words. However, the other threads will lock the + BApplication or using scripting commands if they want to make changes. */ + + bool m_DatabaseHasChanged; + /* Set to TRUE when the in-memory database (stored in m_WordMap) has + changed and is different from the on-disk database file. When the + application exits, the database will be written out if it has changed. */ + + BString m_DatabaseFileName; + /* The absolute path name to use for the database file on disk. */ + + bool m_IgnorePreviousClassification; + /* If TRUE then the previous classification of a message (stored in an + attribute on the message file) will be ignored, and the message will be + added to the requested spam/genuine list. If this is FALSE then the spam + won't be added to the list if it has already been classified as specified, + but if it was mis-classified, it will be removed from the old list and + added to the new list. */ + + uint32 m_OldestAge; + /* The age of the oldest word. This will be the smallest age number in the + database. Mostly useful for scaling graphics representing age in the word + display. If the oldest word is no longer the oldest, this variable won't + get immediately updated since it would take a lot of effort to find the + next older age. Since it's only used for display, we'll let it be slightly + incorrect. The next database load or purge will fix it. */ + + uint32 m_PurgeAge; + /* When purging old words, they have to be at least this old to be eligible + for deletion. Age is measured as the number of e-mails added to the + database since the word was last updated in the database. Zero means all + words are old. */ + + uint32 m_PurgePopularity; + /* When purging old words, they have to be less than or equal to this + popularity limit to be eligible for deletion. Popularity is measured as + the number of messages (spam and genuine) which have the word. Zero means + no words. */ + + ScoringModes m_ScoringMode; + /* Controls how to combine the word probabilities into an overall score. + See the PN_SCORING_MODE comments for details. */ + + BPath m_SettingsDirectoryPath; + /* The constructor initialises this to the settings directory path. It + never changes after that. */ + + bool m_SettingsHaveChanged; + /* Set to TRUE when the settings are changed (different than the ones which + were loaded). When the application exits, the settings will be written out + if they have changed. */ + + double m_SmallestUseableDouble; + /* When multiplying fractional numbers together, avoid using numbers + smaller than this because the double exponent range is close to being + exhausted. The IEEE STANDARD 754 floating-point arithmetic (used on the + Intel i8087 and later math processors) has 64 bit numbers with 53 bits of + mantissa, giving it an underflow starting at 0.5**1022 = 2.2e-308 where it + rounds off to the nearest multiple of 0.5**1074 = 4.9e-324. */ + + bool m_SpaceCharacters [128]; + /* An array of flags to identify characters which are considered to be + spaces. If character code X has m_SpaceCharacters[X] set to true then it + is a space-like character. Character codes 128 and above are always + non-space since they are UTF-8 characters. */ + + TokenizeModes m_TokenizeMode; + /* Controls how to convert the raw message text into words. See the + PN_TOKENIZE_MODE comments for details. */ + + uint32 m_TotalGenuineMessages; + /* Number of genuine messages which are in the database. */ + + uint32 m_TotalSpamMessages; + /* Number of spam messages which are in the database. */ + + uint32 m_WordCount; + /* The number of words currently in the database. Stored separately as a + member variable to avoid having to call m_WordMap.size() all the time, + which other threads can't do while the database is being updated (but they + can look at the word count variable). */ + + StatisticsMap m_WordMap; + /* The in-memory data structure holding the set of words and their + associated statistics. When the database isn't in use, it is an empty + collection. You should lock the BApplication if you are using the word + collection (reading or writing) from another thread. */ +}; + + + +/****************************************************************************** + * Global utility function to display an error message and return. The message + * part describes the error, and if ErrorNumber is non-zero, gets the string + * ", error code $X (standard description)." appended to it. If the message + * is NULL then it gets defaulted to "Something went wrong". The title part + * doesn't get displayed (no title bar in the dialog box, but you can see it in + * the debugger as the window thread name), and defaults to "Error Message" if + * you didn't specify one. If running in command line mode, the error gets + * printed to stderr rather than showing up in a dialog box. + */ + +static void DisplayErrorMessage ( + const char *MessageString = NULL, + int ErrorNumber = 0, + const char *TitleString = NULL) +{ + BAlert *AlertPntr; + char ErrorBuffer [PATH_MAX + 1500]; + + if (TitleString == NULL) + TitleString = "AGMSBayesianSpamServer Error Message"; + + if (MessageString == NULL) + { + if (ErrorNumber == 0) + MessageString = "No error, no message, why bother?"; + else + MessageString = "Something went wrong"; + } + + if (ErrorNumber != 0) + { + sprintf (ErrorBuffer, "%s, error code $%X/%d (%s) has occured.", + MessageString, ErrorNumber, ErrorNumber, strerror (ErrorNumber)); + MessageString = ErrorBuffer; + } + + if (g_CommandLineMode || g_ServerMode) + cerr << TitleString << ": " << MessageString << endl; + else + { + AlertPntr = new BAlert (TitleString, MessageString, + "Acknowledge", NULL, NULL, B_WIDTH_AS_USUAL, B_STOP_ALERT); + if (AlertPntr != NULL) + AlertPntr->Go (); + } +} + + + +/****************************************************************************** + * Word wrap a long line of text into shorter 79 column lines and print the + * result on the given output stream. + */ + +static void WrapTextToStream (ostream& OutputStream, char *TextPntr) +{ + const int LineLength = 79; + char *StringPntr; + char TempString [LineLength+1]; + + TempString[LineLength] = 0; /* Only needs to be done once. */ + + while (*TextPntr != 0) + { + while (isspace (*TextPntr)) + TextPntr++; /* Skip leading spaces. */ + if (*TextPntr == 0) + break; /* It was all spaces, don't print any more. */ + + strncpy (TempString, TextPntr, LineLength); + + /* Advance StringPntr to the end of the temp string, partly to see how long + it is (rather than doing strlen). */ + + StringPntr = TempString; + while (*StringPntr != 0) + StringPntr++; + + if (StringPntr - TempString < LineLength) + { + /* This line fits completely. */ + OutputStream << TempString << endl; + TextPntr += StringPntr - TempString; + continue; + } + + /* Advance StringPntr to the last space in the temp string. */ + + while (StringPntr > TempString) + { + if (isspace (*StringPntr)) + break; /* Found the trailing space. */ + else /* Go backwards, looking for the trailing space. */ + StringPntr--; + } + + /* Remove more trailing spaces at the end of the line, in case there were + several spaces in a row. */ + + while (StringPntr > TempString && isspace (StringPntr[-1])) + StringPntr--; + + /* Print the line of text and advance the text pointer too. */ + + if (StringPntr == TempString) + { + /* This line has no spaces, don't wrap it, just split off a chunk. */ + OutputStream << TempString << endl; + TextPntr += strlen (TempString); + continue; + } + + *StringPntr = 0; /* Cut off after the first trailing space. */ + OutputStream << TempString << endl; + TextPntr += StringPntr - TempString; + } +} + + + +/****************************************************************************** + * Print the usage info to the stream. Includes a list of all commands. + */ +ostream& PrintUsage (ostream& OutputStream); + +ostream& PrintUsage (ostream& OutputStream) +{ + struct property_info *PropInfoPntr; + + OutputStream << "\nAGMSBayesianSpamServer - A Spam Database Server\n"; + OutputStream << "Copyright © 2002 by Alexander G. M. Smith. "; + OutputStream << "Released to the public domain.\n\n"; + WrapTextToStream (OutputStream, "Compiled on " __DATE__ " at " __TIME__ +". $Revision: 1.1 $ $Header: /tmp/bonefish/open-beos/current/src/apps/bin/spamfilter.cpp,v 1.1 2004/11/12 02:55:04 nwhitehorn Exp $"); + OutputStream << "\n" +"This is a program for classifying e-mail messages as spam (junk mail which\n" +"you don't want to read) and regular genuine messages. It can learn what's\n" +"spam and what's genuine. You just give it a bunch of spam messages and a\n" +"bunch of non-spam ones. It uses them to make a list of the words from the\n" +"messages with the probability that each word is from a spam message or from\n" +"a genuine message. Later on, it can use those probabilities to classify\n" +"new messages as spam or not spam. If the classifier stops working well\n" +"(because the spammers have changed their writing style and vocabulary, or\n" +"your regular correspondants are writing like spammers), you can use this\n" +"program to update the list of words to identify the new messages\n" +"correctly.\n" +"\n" +"The original idea was from Paul Graham's algorithm, which has an excellent\n" +"writeup at: http://www.paulgraham.com/spam.html\n" +"\n" +"Gary Robinson came up with the improved algorithm, which you can read about at:\n" +"http://radio.weblogs.com/0101454/stories/2002/09/16/spamDetection.html\n" +"\n" +"Then he, Tim Peters and the SpamBayes mailing list developed the Chi-Squared\n" +"test, see http://mail.python.org/pipermail/spambayes/2002-October/001036.html\n" +"for one of the earlier messages leading from the central limit theorem to\n" +"the current chi-squared scoring method.\n" +"\n" +"Thanks go to Isaac Yonemoto for providing a better icon, which we can\n" +"unfortunately no longer use, since the Hormel company wants people to\n" +"avoid associating their meat product with junk e-mail.\n" +"\n" +"Usage: Specify the operation as the first argument followed by more\n" +"information as appropriate. The program's configuration will affect the\n" +"actual operation (things like the name of the database file to use, or\n" +"whether it should allow non-email messages to be added). In command line\n" +"mode it will do the operation and exit. In GUI/server mode a command line\n" +"invocation will just send the command to the running server. You can also\n" +"use BeOS scripting (see the \"Hey\" command which you can get from\n" +"http://www.bebits.com/app/2042 ) to control the Spam server. And finally,\n" +"there's also a GUI interface which shows up if you start it without any\n" +"command line arguments.\n" +"\n" +"Commands:\n" +"\n" +"Quit\n" +"Stop the program. Useful if it's running as a server.\n" +"\n"; + + /* Go through all our scripting commands and add a description of each one to + the usage text. */ + + for (PropInfoPntr = g_ScriptingPropertyList + 0; + PropInfoPntr->name != 0; + PropInfoPntr++) + { + switch (PropInfoPntr->commands[0]) + { + case B_GET_PROPERTY: + OutputStream << "Get " << PropInfoPntr->name << endl; + break; + + case B_SET_PROPERTY: + OutputStream << "Set " << PropInfoPntr->name << " NewValue" << endl; + break; + + case B_COUNT_PROPERTIES: + OutputStream << "Count " << PropInfoPntr->name << endl; + break; + + case B_CREATE_PROPERTY: + OutputStream << "Create " << PropInfoPntr->name << endl; + break; + + case B_DELETE_PROPERTY: + OutputStream << "Delete " << PropInfoPntr->name << endl; + break; + + case B_EXECUTE_PROPERTY: + OutputStream << PropInfoPntr->name << endl; + break; + + default: + OutputStream << "Buggy Command: " << PropInfoPntr->name << endl; + break; + } + WrapTextToStream (OutputStream, PropInfoPntr->usage); + OutputStream << endl; + } + + return OutputStream; +} + + + +/****************************************************************************** + * A utility function to send a command to the application, will return after a + * short delay if the application is busy (doesn't wait for it to be executed). + * The reply from the application is also thrown away. It used to be an + * overloaded function, but the system couldn't distinguish between bool and + * int, so now it has slightly different names depending on the arguments. + */ + +static void SubmitCommand (BMessage& CommandMessage) +{ + status_t ErrorCode; + + ErrorCode = be_app_messenger.SendMessage (&CommandMessage, + be_app_messenger /* reply messenger, throw away the reply */, + 1000000 /* delivery timeout */); + + if (ErrorCode != B_OK) + cerr << "SubmitCommand failed to send a command, code " << + ErrorCode << " (" << strerror (ErrorCode) << ")." << endl; +} + + +static void SubmitCommandString ( + PropertyNumbers Property, + uint32 CommandCode, + const char *StringArgument = NULL) +{ + BMessage CommandMessage (CommandCode); + + if (Property < 0 || Property >= PN_MAX) + { + DisplayErrorMessage ("SubmitCommandString bug."); + return; + } + CommandMessage.AddSpecifier (g_PropertyNames [Property]); + if (StringArgument != NULL) + CommandMessage.AddString (g_DataName, StringArgument); + SubmitCommand (CommandMessage); +} + + +static void SubmitCommandInt32 ( + PropertyNumbers Property, + uint32 CommandCode, + int32 Int32Argument) +{ + BMessage CommandMessage (CommandCode); + + if (Property < 0 || Property >= PN_MAX) + { + DisplayErrorMessage ("SubmitCommandInt32 bug."); + return; + } + CommandMessage.AddSpecifier (g_PropertyNames [Property]); + CommandMessage.AddInt32 (g_DataName, Int32Argument); + SubmitCommand (CommandMessage); +} + + +static void SubmitCommandBool ( + PropertyNumbers Property, + uint32 CommandCode, + bool BoolArgument) +{ + BMessage CommandMessage (CommandCode); + + if (Property < 0 || Property >= PN_MAX) + { + DisplayErrorMessage ("SubmitCommandBool bug."); + return; + } + CommandMessage.AddSpecifier (g_PropertyNames [Property]); + CommandMessage.AddBool (g_DataName, BoolArgument); + SubmitCommand (CommandMessage); +} + + + +/****************************************************************************** + * A utility function which will estimate the spaminess of file(s), not + * callable from the application thread since it sends a scripting command to + * the application and waits for results. For each file there will be an entry + * reference in the message. For each of those, run it through the spam + * estimator and display a box with the results. This function is used both by + * the file requestor and by dragging and dropping into the middle of the words + * view. + */ + +static void EstimateRefFilesAndDisplay (BMessage *MessagePntr) +{ + BAlert *AlertPntr; + BEntry Entry; + entry_ref EntryRef; + status_t ErrorCode; + int i, j; + BPath Path; + BMessage ReplyMessage; + BMessage ScriptingMessage; + const char *StringPntr; + float TempFloat; + int32 TempInt32; + char TempString [PATH_MAX + 1024 + + g_MaxInterestingWords * (g_MaxWordLength + 16)]; + + for (i = 0; MessagePntr->FindRef ("refs", i, &EntryRef) == B_OK; i++) + { + /* See if the entry is a valid file or directory or other thing. */ + + ErrorCode = Entry.SetTo (&EntryRef, true /* traverse symbolic links */); + if (ErrorCode != B_OK || !Entry.Exists () || Entry.GetPath (&Path) != B_OK) + continue; + + /* Evaluate the spaminess of the file. */ + + ScriptingMessage.MakeEmpty (); + ScriptingMessage.what = B_SET_PROPERTY; + ScriptingMessage.AddSpecifier (g_PropertyNames[PN_EVALUATE]); + ScriptingMessage.AddString (g_DataName, Path.Path ()); + + if (be_app_messenger.SendMessage (&ScriptingMessage,&ReplyMessage) != B_OK) + break; /* App has died or something is wrong. */ + + if (ReplyMessage.FindInt32 ("error", &TempInt32) != B_OK || + TempInt32 != B_OK) + break; /* Error messages will be displayed elsewhere. */ + + ReplyMessage.FindFloat (g_ResultName, &TempFloat); + sprintf (TempString, "%f spam ratio for \"%s\".\nThe top words are:", + (double) TempFloat, Path.Path ()); + + for (j = 0; j < 20 /* Don't print too many! */; j++) + { + if (ReplyMessage.FindString ("words", j, &StringPntr) != B_OK || + ReplyMessage.FindFloat ("ratios", j, &TempFloat) != B_OK) + break; + + sprintf (TempString + strlen (TempString), "\n%s / %f", + StringPntr, TempFloat); + } + if (j >= 20 && j < g_MaxInterestingWords) + sprintf (TempString + strlen (TempString), "\nAnd up to %d more words.", + g_MaxInterestingWords - j); + + AlertPntr = new BAlert ("Estimate", TempString, "OK"); + if (AlertPntr != NULL) + AlertPntr->Go (); + } +} + + + +/****************************************************************************** + * A utility function from the http://sourceforge.net/projects/spambayes + * SpamBayes project. Return prob(chisq >= x2, with v degrees of freedom). It + * computes the probability that the chi-squared value (a kind of normalized + * error measurement), with v degrees of freedom, would be larger than a given + * number (x2; chi is the Greek letter X thus x2). So you can tell if the + * error is really unusual (the returned probability is near zero meaning that + * your measured error number is kind of large - actual chi-squared is rarely + * above that number merely due to random effects), or if it happens often + * (usually if the probability is over 5% then it's within 3 standard + * deviations - meaning that chi-squared goes over your number fairly often due + * merely to random effects). v must be even for this calculation to work. + */ + +static double ChiSquaredProbability (double x2, int v) +{ + int halfV = v / 2; + int i; + double m; + double sum; + double term; + + if (v & 1) + return -1.0; /* Out of range return value as a hint v is odd. */ + + /* If x2 is very large, exp(-m) will underflow to 0. */ + m = x2 / 2.0; + sum = term = exp (-m); + for (i = 1; i < halfV; i++) + { + term *= m / i; + sum += term; + } + + /* With small x2 and large v, accumulated roundoff error, plus error in the + platform exp(), can cause this to spill a few ULP above 1.0. For example, + ChiSquaredProbability(100, 300) on my box has sum == 1.0 + 2.0**-52 at this + point. Returning a value even a teensy bit over 1.0 is no good. */ + + if (sum > 1.0) + return 1.0; + return sum; +} + + + +/****************************************************************************** + * A utility function to remove the "[Spam 99.9%] " from in front of the + * MAIL:subject attribute of a file. + */ + +static status_t RemoveSpamPrefixFromSubjectAttribute (BNode *BNodePntr) +{ + status_t ErrorCode; + const char *MailSubjectName = "MAIL:subject"; + char *StringPntr; + char SubjectString [2000]; + + ErrorCode = BNodePntr->ReadAttr (MailSubjectName, + B_STRING_TYPE, 0 /* offset */, SubjectString, + sizeof (SubjectString) - 1); + if (ErrorCode <= 0) + return 0; /* The attribute isn't there so we don't care. */ + if (ErrorCode >= (int) sizeof (SubjectString) - 1) + return 0; /* Can't handle subjects which are too long. */ + + SubjectString [ErrorCode] = 0; + ErrorCode = 0; /* So do-nothing exit returns zero. */ + if (strncmp (SubjectString, "[Spam ", 6) == 0) + { + for (StringPntr = SubjectString; + *StringPntr != 0 && *StringPntr != ']'; StringPntr++) + ; /* No body in this for loop. */ + if (StringPntr[0] == ']' && StringPntr[1] == ' ') + { + ErrorCode = BNodePntr->RemoveAttr (MailSubjectName); + ErrorCode = BNodePntr->WriteAttr (MailSubjectName, + B_STRING_TYPE, 0 /* offset */, + StringPntr + 2, strlen (StringPntr + 2) + 1); + if (ErrorCode > 0) + ErrorCode = 0; + } + } + + return ErrorCode; +} + + + +/****************************************************************************** + * Implementation of the ABSApp class, constructor, destructor and the rest of + * the member functions in mostly alphabetical order. + */ + +ABSApp::ABSApp () +: BApplication (g_ABSAppSignature), + m_DatabaseHasChanged (false), + m_SettingsHaveChanged (false) +{ + status_t ErrorCode; + int HalvingCount; + int i; + + MakeDatabaseEmpty (); + + /* Set up the pathname which identifies our settings directory. Note that + the actual settings are loaded later on (or set to defaults) by the main() + function, before this BApplication starts running. So we don't bother + initialising the other setting related variables here. */ + + ErrorCode = + find_directory (B_USER_SETTINGS_DIRECTORY, &m_SettingsDirectoryPath); + if (ErrorCode == B_OK) + ErrorCode = m_SettingsDirectoryPath.Append (g_SettingsDirectoryName); + if (ErrorCode != B_OK) + m_SettingsDirectoryPath.SetTo ("."); + + /* Set up the table which identifies which characters are spaces and which + are not. Spaces are all control characters and all punctuation except for: + apostrophe (so "it's" and possessive versions of words get stored), dash (for + hyphenated words), dollar sign (for cash amounts), period (for IP addresses, + we later remove trailing periods). */ + + memset (m_SpaceCharacters, 1, sizeof (m_SpaceCharacters)); + m_SpaceCharacters['\''] = false; + m_SpaceCharacters['-'] = false; + m_SpaceCharacters['$'] = false; + m_SpaceCharacters['.'] = false; + for (i = '0'; i <= '9'; i++) + m_SpaceCharacters[i] = false; + for (i = 'A'; i <= 'Z'; i++) + m_SpaceCharacters[i] = false; + for (i = 'a'; i <= 'z'; i++) + m_SpaceCharacters[i] = false; + + /* Find out the smallest usable double by seeing how small we can make it. */ + + m_SmallestUseableDouble = 1.0; + HalvingCount = 0; + while (HalvingCount < 10000 && m_SmallestUseableDouble > 0.0) + { + HalvingCount++; + m_SmallestUseableDouble /= 2; + } + + /* Recreate the number. But don't make quite as small, we want to allow some + precision bits and a bit of extra margin for intermediate results in future + calculations. */ + + HalvingCount -= 50 + sizeof (double) * 8; + + m_SmallestUseableDouble = 1.0; + while (HalvingCount > 0) + { + HalvingCount--; + m_SmallestUseableDouble /= 2; + } +} + + +ABSApp::~ABSApp () +{ + status_t ErrorCode; + char ErrorMessage [PATH_MAX + 1024]; + + if (m_SettingsHaveChanged) + LoadSaveSettings (false /* DoLoad */); + if ((ErrorCode = SaveDatabaseIfNeeded (ErrorMessage)) != B_OK) + DisplayErrorMessage (ErrorMessage, ErrorCode, "Exiting Error"); +} + + +/* Display a box showing information about this program. */ + +void ABSApp::AboutRequested () +{ + BAlert *AboutAlertPntr; + + AboutAlertPntr = new BAlert ("About", +"AGMSBayesianSpamServer\n\n" + +"This is a BeOS program for classifying e-mail messages as spam (unwanted \ +junk mail) or as genuine mail using a Bayesian statistical approach. There \ +is also a Mail Daemon Replacement add-on to filter mail using the \ +classification statistics collected earlier.\n\n" + +"Written by Alexander G. M. Smith, fall 2002.\n\n" + +"The original idea was from Paul Graham's algorithm, which has an excellent \ +writeup at: http://www.paulgraham.com/spam.html\n\n" + +"Gary Robinson came up with the improved algorithm, which you can read about \ +at: http://radio.weblogs.com/0101454/stories/2002/09/16/spamDetection.html\n\n" + +"Mr. Robinson, Tim Peters and the SpamBayes mailing list people then \ +developed the even better chi-squared scoring method.\n\n" + +"Icon courtesy of Isaac Yonemoto, though it is no longer used since Hormel \ +doesn't want their meat product associated with junk e-mail.\n\n" + +"Released to the public domain, with no warranty.\n" +"$Revision: 1.1 $\n" +"Compiled on " __DATE__ " at " __TIME__ ".", "Done"); + if (AboutAlertPntr != NULL) + { + AboutAlertPntr->SetShortcut (0, B_ESCAPE); + AboutAlertPntr->Go (); + } +} + + +/* Add the text in the given file to the database as an example of a spam or +genuine message, or removes it from the database if you claim it is +CL_UNCERTAIN. Also resets the spam ratio attribute to show the effect of the +database change. */ + +status_t ABSApp::AddFileToDatabase ( + ClassificationTypes IsSpamOrWhat, + const char *FileName, + char *ErrorMessage) +{ + status_t ErrorCode; + BFile MessageFile; + BMessage TempBMessage; + + ErrorCode = MessageFile.SetTo (FileName, B_READ_ONLY); + if (ErrorCode != B_OK) + { + sprintf (ErrorMessage, "Unable to open file \"%s\" for reading", FileName); + return ErrorCode; + } + + ErrorCode = AddPositionIOToDatabase (IsSpamOrWhat, + &MessageFile, FileName, ErrorMessage); + MessageFile.Unset (); + if (ErrorCode != B_OK) + return ErrorCode; + + /* Re-evaluate the file so that the user sees the new ratio attribute. */ + return EvaluateFile (FileName, &TempBMessage, ErrorMessage); +} + + +/* Add the given text to the database. The unique words found in MessageIOPntr +will be added to the database (incrementing the count for the number of +messages using each word, either the spam or genuine count depending on +IsSpamOrWhat). It will remove the message (decrement the word counts) if you +specify CL_UNCERTAIN as the new classification. And if it switches from spam +to genuine or vice versa, it will do both - decrement the counts for the old +class and increment the counts for the new one. An attribute will be added to +MessageIOPntr (if it is a file) to record that it has been marked as Spam or +Genuine (so that it doesn't get added to the database a second time). If it is +being removed from the database, the classification attribute gets removed too. +If things go wrong, a non-zero error code will be returned and an explanation +written to ErrorMessage (assumed to be at least PATH_MAX + 1024 bytes long). +OptionalFileName is just used in the error message to identify the file to the +user. */ + +status_t ABSApp::AddPositionIOToDatabase ( + ClassificationTypes IsSpamOrWhat, + BPositionIO *MessageIOPntr, + const char *OptionalFileName, + char *ErrorMessage) +{ + BNode *BNodePntr; + char ClassificationString [NAME_MAX]; + StatisticsMap::iterator DataIter; + status_t ErrorCode = 0; + pair InsertResult; + uint32 NewAge; + StatisticsRecord NewStatistics; + ClassificationTypes PreviousClassification; + StatisticsPointer StatisticsPntr; + set::iterator WordEndIter; + set::iterator WordIter; + set WordSet; + + NewAge = m_TotalGenuineMessages + m_TotalSpamMessages; + if (NewAge >= 0xFFFFFFF0UL) + { + sprintf (ErrorMessage, "The database is full! There are %lu messages in " + "it and we can't add any more without overflowing the maximum integer " + "representation in 32 bits", NewAge); + return ENOMEM; + } + + /* Check that this file hasn't already been added to the database. */ + + PreviousClassification = CL_UNCERTAIN; + BNodePntr = dynamic_cast (MessageIOPntr); + if (BNodePntr != NULL) /* If this thing might have attributes. */ + { + ErrorCode = BNodePntr->ReadAttr (g_AttributeNameClassification, + B_STRING_TYPE, 0 /* offset */, ClassificationString, + sizeof (ClassificationString) - 1); + if (ErrorCode <= 0) /* Positive values for the number of bytes read */ + strcpy (ClassificationString, "none"); + else /* Just in case it needs a NUL at the end. */ + ClassificationString [ErrorCode] = 0; + + if (strcasecmp (ClassificationString, g_ClassifiedSpam) == 0) + PreviousClassification = CL_SPAM; + else if (strcasecmp (ClassificationString, g_ClassifiedGenuine) == 0) + PreviousClassification = CL_GENUINE; + } + + if (!m_IgnorePreviousClassification && + PreviousClassification != CL_UNCERTAIN) + { + if (IsSpamOrWhat == PreviousClassification) + { + sprintf (ErrorMessage, "Ignoring file \"%s\" since it seems to have " + "already been classified as %s.", OptionalFileName, + g_ClassificationTypeNames [IsSpamOrWhat]); + } + else + { + sprintf (ErrorMessage, "Changing existing classification of file \"%s\" " + "from %s to %s.", OptionalFileName, + g_ClassificationTypeNames [PreviousClassification], + g_ClassificationTypeNames [IsSpamOrWhat]); + } + DisplayErrorMessage (ErrorMessage, 0, "Note"); + } + + if (!m_IgnorePreviousClassification && + IsSpamOrWhat == PreviousClassification) + /* Nothing to do if it is already classified correctly and the user doesn't + want double classification. */ + return B_OK; + + /* Get the list of unique words in the file. */ + + ErrorCode = GetWordsFromPositionIO (MessageIOPntr, OptionalFileName, + WordSet, ErrorMessage); + if (ErrorCode != B_OK) + return ErrorCode; + + /* Update the count of the number of messages processed, with corrections if + reclassifying a message. */ + + m_DatabaseHasChanged = true; + + if (!m_IgnorePreviousClassification && + PreviousClassification == CL_SPAM && m_TotalSpamMessages > 0) + m_TotalSpamMessages--; + + if (IsSpamOrWhat == CL_SPAM) + m_TotalSpamMessages++; + + if (!m_IgnorePreviousClassification && + PreviousClassification == CL_GENUINE && m_TotalGenuineMessages > 0) + m_TotalGenuineMessages--; + + if (IsSpamOrWhat == CL_GENUINE) + m_TotalGenuineMessages++; + + /* Mark the file's attributes with the new classification. Don't care if it + fails. */ + + if (BNodePntr != NULL) /* If this thing might have attributes. */ + { + ErrorCode = BNodePntr->RemoveAttr (g_AttributeNameClassification); + if (IsSpamOrWhat != CL_UNCERTAIN) + { + strcpy (ClassificationString, g_ClassificationTypeNames [IsSpamOrWhat]); + ErrorCode = BNodePntr->WriteAttr (g_AttributeNameClassification, + B_STRING_TYPE, 0 /* offset */, + ClassificationString, strlen (ClassificationString) + 1); + } + } + + /* Add the words to the database by incrementing or decrementing the counts + for each word as appropriate. */ + + WordEndIter = WordSet.end (); + for (WordIter = WordSet.begin (); WordIter != WordEndIter; WordIter++) + { + if ((DataIter = m_WordMap.find (*WordIter)) == m_WordMap.end ()) + { + /* No record in the database for the word. */ + + if (IsSpamOrWhat == CL_UNCERTAIN) + continue; /* Not adding words, don't have to subtract from nothing. */ + + /* Create a new one record in the database for the new word. */ + + memset (&NewStatistics, 0, sizeof (NewStatistics)); + InsertResult = m_WordMap.insert ( + StatisticsMap::value_type (*WordIter, NewStatistics)); + if (!InsertResult.second) + { + sprintf (ErrorMessage, "Failed to insert new database entry for " + "word \"%s\", while processing file \"%s\"", + WordIter->c_str (), OptionalFileName); + return ENOMEM; + } + DataIter = InsertResult.first; + m_WordCount++; + } + + /* Got the database record for the word, update the statistics. */ + + StatisticsPntr = &DataIter->second; + + StatisticsPntr->age = NewAge; + + /* Can't update m_OldestAge here, since it would take a lot of effort to + find the next older age. Since it's only used for display, we'll let it be + slightly incorrect. The next database load or purge will fix it. */ + + if (IsSpamOrWhat == CL_SPAM) + StatisticsPntr->spamCount++; + + if (IsSpamOrWhat == CL_GENUINE) + StatisticsPntr->genuineCount++; + + if (!m_IgnorePreviousClassification && + PreviousClassification == CL_SPAM && StatisticsPntr->spamCount > 0) + StatisticsPntr->spamCount--; + + if (!m_IgnorePreviousClassification && + PreviousClassification == CL_GENUINE && StatisticsPntr->genuineCount > 0) + StatisticsPntr->genuineCount--; + } + + return B_OK; +} + + +/* Add the text in the string to the database as an example of a spam or +genuine message. */ + +status_t ABSApp::AddStringToDatabase ( + ClassificationTypes IsSpamOrWhat, + const char *String, + char *ErrorMessage) +{ + BMemoryIO MemoryIO (String, strlen (String)); + + return AddPositionIOToDatabase (IsSpamOrWhat, &MemoryIO, + "Memory Buffer" /* OptionalFileName */, ErrorMessage); +} + + +/* Given a bunch of text, break it up into words without doing special +decoding, and add them to the set. Allow NULs in the text. See also +TokenizeWhole which does something similar. */ + +void ABSApp::AddWordsToSet ( + const char *InputString, + size_t NumberOfBytes, + set &WordSet) +{ + string AccumulatedWord; + const char *EndOfStringPntr; + size_t Length; + int Letter = ' '; + + /* Find all the words in the string and add them to our local set of words. + A set is used since we don't care how many times a word occurs. */ + + EndOfStringPntr = InputString + NumberOfBytes; + while (true) + { + if (InputString >= EndOfStringPntr) + Letter = EOF; + else + Letter = (unsigned char) *InputString++; + + /* Convert to lower case to improve word matches. Of course this loses a + bit of information, such as MONEY vs Money, an indicator of spam. Well, + apparently that is actually a useful distinction, so leave the case alone. + if (Letter >= 0 && Letter < 128) + Letter = tolower (Letter); + */ + + /* See if it is a letter we treat as white space - all control characters + and all punctuation except for: apostrophe (so "it's" and possessive + versions of words get stored), dash (for hyphenated words), dollar sign + (for cash amounts), period (for IP addresses, we later remove trailing + (periods). Note that codes above 127 are UTF-8 characters, which we + consider non-space. */ + + if (Letter < 0 || (Letter < 128 && m_SpaceCharacters[Letter])) + { + /* That space finished off a word. Remove trailing periods... */ + + while ((Length = AccumulatedWord.size()) > 0 && + AccumulatedWord [Length-1] == '.') + AccumulatedWord.resize (Length - 1); + + /* If there's anything left in the word, add it to the set. Also ignore + words which are too big (it's probably some binary encoded data). But + leave room for supercalifragilisticexpialidoceous. According to one web + site, pneumonoultramicroscopicsilicovolcanoconiosis is the longest word + currently in English. Note that some uuencoded data was seen with a 60 + character line length. */ + + if (Length > 0 && Length <= g_MaxWordLength) + WordSet.insert (AccumulatedWord); + + /* Empty out the string to get ready for the next word. */ + + AccumulatedWord.resize (0); + } + else /* Not a space-like character, add it to the word. */ + AccumulatedWord.append (1 /* one copy of the char */, (char) Letter); + + if (Letter < 0) + break; /* End of data. Exit here so that last word got processed. */ + } +} + + +/* The user has provided a command line. This could actually be from a +separate attempt to invoke the program (this application's resource/attributes +have the launch flags set to "single launch", so the shell doesn't start the +program but instead sends the arguments to the already running instance). In +either case, the command is sent to an intermediary thread where it is +asynchronously converted into a scripting message(s) that are sent back to this +BApplication. The intermediary is needed since we can't recursively execute +scripting messages while processing a message (this ArgsReceived one). */ + +void ABSApp::ArgvReceived (int32 argc, char **argv) +{ + if (g_CommanderLooperPntr != NULL) + g_CommanderLooperPntr->CommandArguments (argc, argv); +} + + +/* Create a new empty database. Note that we have to write out the new file +immediately, otherwise other operations will see the empty database and then +try to load the file, and complain that it doesn't exist. Now they will see +the empty database and redundantly load the empty file. */ + +status_t ABSApp::CreateDatabaseFile (char *ErrorMessage) +{ + MakeDatabaseEmpty (); + m_DatabaseHasChanged = true; + return SaveDatabaseIfNeeded (ErrorMessage); /* Make it now. */ +} + + +/* Set the settings to the defaults. Needed in case there isn't a settings +file or it is obsolete. */ + +void ABSApp::DefaultSettings () +{ + status_t ErrorCode; + BPath DatabasePath (m_SettingsDirectoryPath); + char TempString [PATH_MAX]; + + /* The default database file is in the settings directory. */ + + ErrorCode = DatabasePath.Append (g_DefaultDatabaseFileName); + if (ErrorCode != B_OK) + strcpy (TempString, g_DefaultDatabaseFileName); /* Unlikely to happen. */ + else + strcpy (TempString, DatabasePath.Path ()); + m_DatabaseFileName.SetTo (TempString); + + m_IgnorePreviousClassification = false; + g_ServerMode = true; + m_PurgeAge = 2000; + m_PurgePopularity = 2; + m_ScoringMode = SM_CHISQUARED; + m_TokenizeMode = TM_ANY_TEXT_HEADER; + + m_SettingsHaveChanged = true; +} + + +/* Deletes the database file, and the backup file, and clears the database but +marks it as not changed so that it doesn't get written out when the program +exits. */ + +status_t ABSApp::DeleteDatabaseFile (char *ErrorMessage) +{ + BEntry FileEntry; + status_t ErrorCode; + int i; + char TempString [PATH_MAX+20]; + + /* Clear the in-memory database. */ + + MakeDatabaseEmpty (); + m_DatabaseHasChanged = false; + + /* Delete the backup files first. Don't care if it fails. */ + + for (i = 0; i < g_MaxBackups; i++) + { + strcpy (TempString, m_DatabaseFileName.String ()); + sprintf (TempString + strlen (TempString), g_BackupSuffix, i); + ErrorCode = FileEntry.SetTo (TempString); + if (ErrorCode == B_OK) + FileEntry.Remove (); + } + + /* Delete the main database file. */ + + strcpy (TempString, m_DatabaseFileName.String ()); + ErrorCode = FileEntry.SetTo (TempString); + if (ErrorCode != B_OK) + { + sprintf (ErrorMessage, "While deleting, failed to make BEntry for " + "\"%s\" (does the directory exist?)", TempString); + return ErrorCode; + } + + ErrorCode = FileEntry.Remove (); + if (ErrorCode != B_OK) + sprintf (ErrorMessage, "While deleting, failed to remove file " + "\"%s\"", TempString); + + return ErrorCode; +} + + +/* Evaluate the given file as being a spam message, and tag it with the +resulting spam probability ratio. If it also has an e-mail subject attribute, +remove the [Spam 99.9%] prefix since the number usually changes. */ + +status_t ABSApp::EvaluateFile ( + const char *PathName, + BMessage *ReplyMessagePntr, + char *ErrorMessage) +{ + status_t ErrorCode; + float TempFloat; + BFile TextFile; + + /* Open the specified file. */ + + ErrorCode = TextFile.SetTo (PathName, B_READ_ONLY); + if (ErrorCode != B_OK) + { + sprintf (ErrorMessage, "Problems opening file \"%s\" for evaluating", + PathName); + return ErrorCode; + } + + ErrorCode = + EvaluatePositionIO (&TextFile, PathName, ReplyMessagePntr, ErrorMessage); + + if (ErrorCode == B_OK && + ReplyMessagePntr->FindFloat (g_ResultName, &TempFloat) == B_OK) + { + TextFile.WriteAttr (g_AttributeNameSpamRatio, B_FLOAT_TYPE, + 0 /* offset */, &TempFloat, sizeof (TempFloat)); + /* Don't know the spam cutoff ratio, that's in the e-mail filter, so just + blindly remove the prefix, which would have the wrong percentage. */ + RemoveSpamPrefixFromSubjectAttribute (&TextFile); + } + + return ErrorCode; +} + + +/* Evaluate a given file or memory buffer (a BPositionIO handles both cases) +for spaminess. The output is added to the ReplyMessagePntr message, with the +probability ratio stored in "result" (0.0 means genuine and 1.0 means spam). +It also adds the most significant words (used in the ratio calculation) to the +array "words" and the associated per-word probability ratios in "ratios". If +it fails, an error code is returned and an error message written to the +ErrorMessage string (which is at least MAX_PATH + 1024 bytes long). +OptionalFileName is only used in the error message. + +The math used for combining the individual word probabilities in my method is +based on Gary Robinson's method (formerly it was a variation of Paul Graham's +method) or the Chi-Squared method. It's input is the database of words that +has a count of the number of spam and number of genuine messages each word +appears in (doesn't matter if it appears more than once in a message, it still +counts as 1). + +The spam word count is divided the by the total number of spam e-mail messages +in the database to get the probability of spam and probability of genuineness +is similarly computed for a particular word. The spam probability is divided +by the sum of the spam and genuine probabilities to get the Raw Spam Ratio for +the word. It's nearer to 0.0 for genuine and nearer to 1.0 for spam, and can +be exactly zero or one too. + +To avoid multiplying later results by zero, and to compensate for a lack of +data points, the Raw Spam Ratio is adjusted towards the 0.5 halfway point. The +0.5 is combined with the raw spam ratio, with a weight of 0.45 (determined to +be a good value by the "spambayes" mailing list tests) messages applied to the +half way point and a weight of the number of spam + genuine messages applied to +the raw spam ratio. This gives you the compensated spam ratio for the word. + +The top N (150 was good in the spambayes tests) extreme words are selected by +the distance of each word's compensated spam ratio from 0.5. Then the ratios +of the words are combined. + +The Gary Robinson combining (scoring) method gets one value from the Nth root +of the product of all the word ratios. The other is the Nth root of the +product of (1 - ratio) for all the words. The final result is the first value +divided by the sum of the two values. The Nth root helps spread the resulting +range of values more evenly between 0.0 and 1.0, otherwise the values all clump +together at 0 or 1. Also you can think of the Nth root as a kind of average +for products; it's like a generic word probability which when multiplied by +itself N times gives you the same result as the N separate actual word +probabilities multiplied together. + +The Chi-Squared combining (scoring) method assumes that the spam word +probabilities are uniformly distributed and computes an error measurement +(called chi squared - see http://bmj.com/collections/statsbk/8.shtml for a good +tutorial) and then sees how likely that error value would be observed in +practice. If it's rare to observe, then the words are likely not just randomly +occuring and it's spammy. The same is done for genuine words. The two +resulting unlikelynesses are compared to see which is more unlikely, if neither +is, then the method says it can't decide. The SpamBayes notes (see the +classifier.py file in CVS in http://sourceforge.net/projects/spambayes) say: + +"Across vectors of length n, containing random uniformly-distributed +probabilities, -2*sum(ln(p_i)) follows the chi-squared distribution with 2*n +degrees of freedom. This has been proven (in some appropriate sense) to be the +most sensitive possible test for rejecting the hypothesis that a vector of +probabilities is uniformly distributed. Gary Robinson's original scheme was +monotonic *with* this test, but skipped the details. Turns out that getting +closer to the theoretical roots gives a much sharper classification, with a +very small (in # of msgs), but also very broad (in range of scores), "middle +ground", where most of the mistakes live. In particular, this scheme seems +immune to all forms of "cancellation disease": if there are many strong ham +*and* spam clues, this reliably scores close to 0.5. Most other schemes are +extremely certain then -- and often wrong." + +I did a test with 448 example genuine messages including personal mail (some +with HTML attachments) and mailing lists, and 267 spam messages for 27471 words +total. Test messages were more recent messages in the same groups. Out of 100 +test genuine messages, with Gary Robinson (0.56 cutoff limit), 1 (1%) was +falsely identified as spam and 8 of 73 (11%) spam messages were incorrectly +classified as genuine. With my variation of Paul Graham's scheme (0.90 cutoff) +I got 6 of 100 (6%) genuine messages incorrectly marked as spam and 2 of 73 +(3%) spam messages were incorrectly classified as genuine. Pretty close, but +Robinson's values are more evenly spread out so you can tell just how spammy it +is by looking at the number. */ + +struct WordAndRatioStruct +{ + double probabilityRatio; /* Actually the compensated ratio. */ + const string *wordPntr; + + bool operator() ( /* Our less-than comparison function for sorting. */ + const WordAndRatioStruct &ItemA, + const WordAndRatioStruct &ItemB) const + { + return + (fabs (ItemA.probabilityRatio - 0.5) < + fabs (ItemB.probabilityRatio - 0.5)); + }; +}; + +status_t ABSApp::EvaluatePositionIO ( + BPositionIO *PositionIOPntr, + const char *OptionalFileName, + BMessage *ReplyMessagePntr, + char *ErrorMessage) +{ + StatisticsMap::iterator DataEndIter; + StatisticsMap::iterator DataIter; + status_t ErrorCode; + double GenuineProbability; + uint32 GenuineSpamSum; + int i; + priority_queue< + WordAndRatioStruct /* Data type stored in the queue */, + vector /* Underlying container */, + WordAndRatioStruct /* Function for comparing elements */> + PriorityQueue; + double ProductGenuine; + double ProductLogGenuine; + double ProductLogSpam; + double ProductSpam; + double RawProbabilityRatio; + float ResultRatio; + double SpamProbability; + StatisticsPointer StatisticsPntr; + double TempDouble; + double TotalGenuine; + double TotalSpam; + WordAndRatioStruct WordAndRatio; + set::iterator WordEndIter; + set::iterator WordIter; + const WordAndRatioStruct *WordRatioPntr; + set WordSet; + + /* Get the list of unique words in the file / memory buffer. */ + + ErrorCode = GetWordsFromPositionIO (PositionIOPntr, OptionalFileName, + WordSet, ErrorMessage); + if (ErrorCode != B_OK) + return ErrorCode; + + /* Prepare a few variables. Mostly these are stored double values of some of + the numbers involved (to avoid the overhead of multiple conversions from + integer to double), with extra precautions to avoid divide by zero. */ + + if (m_TotalGenuineMessages <= 0) + TotalGenuine = 1.0; + else + TotalGenuine = m_TotalGenuineMessages; + + if (m_TotalSpamMessages <= 0) + TotalSpam = 1.0; + else + TotalSpam = m_TotalSpamMessages; + + /* Look up the words in the database and calculate their compensated spam + ratio. The results are stored in a priority queue so that we can later find + the top g_MaxInterestingWords for doing the actual determination. */ + + WordEndIter = WordSet.end (); + DataEndIter = m_WordMap.end (); + for (WordIter = WordSet.begin (); WordIter != WordEndIter; WordIter++) + { + WordAndRatio.wordPntr = &(*WordIter); + + if ((DataIter = m_WordMap.find (*WordIter)) != DataEndIter) + { + StatisticsPntr = &DataIter->second; + + /* Calculate the probability the word is spam and the probability it is + genuine. Then the raw probability ratio. */ + + SpamProbability = StatisticsPntr->spamCount / TotalSpam; + GenuineProbability = StatisticsPntr->genuineCount / TotalGenuine; + + if (SpamProbability + GenuineProbability > 0) + RawProbabilityRatio = + SpamProbability / (SpamProbability + GenuineProbability); + else /* Word with zero statistics, perhaps due to reclassification. */ + RawProbabilityRatio = 0.5; + + /* The compensated ratio leans towards 0.5 (g_RobinsonX) more for fewer + data points, with a weight of 0.45 (g_RobinsonS). */ + + GenuineSpamSum = + StatisticsPntr->spamCount + StatisticsPntr->genuineCount; + + WordAndRatio.probabilityRatio = + (g_RobinsonS * g_RobinsonX + GenuineSpamSum * RawProbabilityRatio) / + (g_RobinsonS + GenuineSpamSum); + } + else /* Unknown word. With N=0, compensated ratio equation is RobinsonX. */ + WordAndRatio.probabilityRatio = g_RobinsonX; + + PriorityQueue.push (WordAndRatio); + } + + /* Compute the combined probability (multiply them together) of the top few + words. To avoid numeric underflow (doubles can only get as small as 1E-300), + logarithms are also used. But avoid the logarithms (sum of logs of numbers + is the same as the product of numbers) as much as possible due to reduced + accuracy and slowness. */ + + ProductGenuine = 1.0; + ProductLogGenuine = 0.0; + ProductSpam = 1.0; + ProductLogSpam = 0.0; + for (i = 0; + i < g_MaxInterestingWords && !PriorityQueue.empty(); + i++, PriorityQueue.pop()) + { + WordRatioPntr = &PriorityQueue.top(); + ProductSpam *= WordRatioPntr->probabilityRatio; + ProductGenuine *= 1.0 - WordRatioPntr->probabilityRatio; + + /* Check for the numbers getting dangerously small, close to underflowing. + If they are, move the value into the logarithm storage part. */ + + if (ProductSpam < m_SmallestUseableDouble) + { + ProductLogSpam += log (ProductSpam); + ProductSpam = 1.0; + } + + if (ProductGenuine < m_SmallestUseableDouble) + { + ProductLogGenuine += log (ProductGenuine); + ProductGenuine = 1.0; + } + + ReplyMessagePntr->AddString ("words", WordRatioPntr->wordPntr->c_str ()); + ReplyMessagePntr->AddFloat ("ratios", WordRatioPntr->probabilityRatio); + } + + /* Get the resulting log of the complete products. */ + + if (i > 0) + { + ProductLogSpam += log (ProductSpam); + ProductLogGenuine += log (ProductGenuine); + } + + if (m_ScoringMode == SM_ROBINSON) + { + /* Apply Gary Robinson's scoring method where we take the Nth root of the + products. This is easiest in logarithm form. */ + + if (i > 0) + { + ProductSpam = exp (ProductLogSpam / i); + ProductGenuine = exp (ProductLogGenuine / i); + ResultRatio = ProductSpam / (ProductGenuine + ProductSpam); + } + else /* Somehow got no words! */ + ResultRatio = g_RobinsonX; + } + else if (m_ScoringMode == SM_CHISQUARED) + { + /* From the SpamBayes notes: "We compute two chi-squared statistics, one + for ham and one for spam. The sum-of-the-logs business is more sensitive + to probs near 0 than to probs near 1, so the spam measure uses 1-p (so that + high-spamprob words have greatest effect), and the ham measure uses p + directly (so that lo-spamprob words have greatest effect)." That means we + just reversed the meaning of the previously calculated spam and genuine + products! Oh well. */ + + TempDouble = ProductLogSpam; + ProductLogSpam = ProductLogGenuine; + ProductLogGenuine = TempDouble; + + if (i > 0) + { + ProductSpam = + 1.0 - ChiSquaredProbability (-2.0 * ProductLogSpam, 2 * i); + ProductGenuine = + 1.0 - ChiSquaredProbability (-2.0 * ProductLogGenuine, 2 * i); + + /* The SpamBayes notes say: "How to combine these into a single spam + score? We originally used (S-H)/(S+H) scaled into [0., 1.], which equals + S/(S+H). A systematic problem is that we could end up being near-certain + a thing was (for example) spam, even if S was small, provided that H was + much smaller. Rob Hooft stared at these problems and invented the + measure we use now, the simpler S-H, scaled into [0., 1.]." */ + + ResultRatio = (ProductSpam - ProductGenuine + 1.0) / 2.0; + } + else /* No words to analyse. */ + ResultRatio = 0.5; + } + else /* Unknown scoring mode. */ + { + strcpy (ErrorMessage, "Unknown scoring mode specified in settings"); + return B_BAD_VALUE; + } + + ReplyMessagePntr->AddFloat (g_ResultName, ResultRatio); + return B_OK; +} + + +/* Just evaluate the given string as being spam text. */ + +status_t ABSApp::EvaluateString ( + const char *BufferPntr, + ssize_t BufferSize, + BMessage *ReplyMessagePntr, + char *ErrorMessage) +{ + BMemoryIO MemoryIO (BufferPntr, BufferSize); + + return EvaluatePositionIO (&MemoryIO, "Memory Buffer", + ReplyMessagePntr, ErrorMessage); +} + + +/* Tell other programs about the scripting commands we support. Try this +command: "hey application/x-vnd.agmsmith.AGMSBayesianSpamServer getsuites" to +see it in action (this program has to be already running for it to work). */ + +status_t ABSApp::GetSupportedSuites (BMessage *MessagePntr) +{ + BPropertyInfo TempPropInfo (g_ScriptingPropertyList); + + MessagePntr->AddString ("suites", + "suite/x-vnd.agmsmith.AGMSBayesianSpamServer"); + MessagePntr->AddFlat ("messages", &TempPropInfo); + return BApplication::GetSupportedSuites (MessagePntr); +} + + +/* Add all the words in the given file or memory buffer to the supplied set. +The file name is only there for error messages, it assumes you have already +opened the PositionIO to the right file. If things go wrong, a non-zero error +code will be returned and an explanation written to ErrorMessage (assumed to be +at least PATH_MAX + 1024 bytes long). */ + +status_t ABSApp::GetWordsFromPositionIO ( + BPositionIO *PositionIOPntr, + const char *OptionalFileName, + set &WordSet, + char *ErrorMessage) +{ + status_t ErrorCode; + + if (m_TokenizeMode == TM_WHOLE) + ErrorCode = TokenizeWhole (PositionIOPntr, OptionalFileName, + WordSet, ErrorMessage); + else + ErrorCode = TokenizeParts (PositionIOPntr, OptionalFileName, + WordSet, ErrorMessage); + + if (ErrorCode == B_OK && WordSet.empty ()) + { + /* ENOMSG usually means no message found in queue, but I'm using it to show + no words, a good indicator of spam which is pure HTML. */ + + sprintf (ErrorMessage, "No words were found in \"%s\"", OptionalFileName); + ErrorCode = ENOMSG; + } + + return ErrorCode; +} + + +/* Set up indices for attributes MAIL:classification (string) and +MAIL:ratio_spam (float) on all mounted disk volumes that support queries. Also +tell the system to make those attributes visible to the user (so they can see +them in Tracker) and associate them with e-mail messages. Also set up the +database file MIME type (provide a description and associate it with this +program so that it picks up the right icon). And register the names for our +sound effects. */ + +status_t ABSApp::InstallThings (char *ErrorMessage) +{ + int32 Cookie; + dev_t DeviceID; + status_t ErrorCode = B_OK; + fs_info FSInfo; + int32 i; + int32 iClassification; + int32 iProbability; + int32 j; + index_info IndexInfo; + BMimeType MimeType; + BMessage Parameters; + const char *StringPntr; + bool TempBool; + int32 TempInt32; + + /* Iterate through all mounted devices and try to make the indices on each + one. Don't bother if the index exists or the device doesn't support indices + (actually queries). */ + + Cookie = 0; + while ((DeviceID = next_dev (&Cookie)) >= 0) + { + if (!fs_stat_dev (DeviceID, &FSInfo) && (FSInfo.flags & B_FS_HAS_QUERY)) + { + if (fs_stat_index (DeviceID, g_AttributeNameClassification, &IndexInfo) + && errno == B_ENTRY_NOT_FOUND) + { + if (fs_create_index (DeviceID, g_AttributeNameClassification, + B_STRING_TYPE, 0 /* flags */)) + { + ErrorCode = errno; + sprintf (ErrorMessage, "Unable to make string index %s on " + "volume #%d, volume name \"%s\", file system type \"%s\", " + "on device \"%s\"", g_AttributeNameClassification, + (int) DeviceID, FSInfo.volume_name, FSInfo.fsh_name, + FSInfo.device_name); + } + } + + if (fs_stat_index (DeviceID, g_AttributeNameSpamRatio, + &IndexInfo) && errno == B_ENTRY_NOT_FOUND) + { + if (fs_create_index (DeviceID, g_AttributeNameSpamRatio, + B_FLOAT_TYPE, 0 /* flags */)) + { + ErrorCode = errno; + sprintf (ErrorMessage, "Unable to make float index %s on " + "volume #%d, volume name \"%s\", file system type \"%s\", " + "on device \"%s\"", g_AttributeNameSpamRatio, + (int) DeviceID, FSInfo.volume_name, FSInfo.fsh_name, + FSInfo.device_name); + } + } + } + } + if (ErrorCode != B_OK) + return ErrorCode; + + /* Set up the MIME types for the classification attributes, associate them + with e-mail and make them visible to the user (but not editable). First need + to get the existing MIME settings, then add ours to them (otherwise the + existing ones get wiped out). */ + + ErrorCode = MimeType.SetTo ("text/x-email"); + if (ErrorCode != B_OK || !MimeType.IsInstalled ()) + { + sprintf (ErrorMessage, "No e-mail MIME type (%s) in the system, can't " + "update it to add our special attributes, and without e-mail this " + "program is useless!", MimeType.Type ()); + if (ErrorCode == B_OK) + ErrorCode = -1; + return ErrorCode; + } + + ErrorCode = MimeType.GetAttrInfo (&Parameters); + if (ErrorCode != B_OK) + { + sprintf (ErrorMessage, "Unable to retrieve list of attributes " + "associated with e-mail messages in the MIME database"); + return ErrorCode; + } + + for (i = 0, iClassification = -1, iProbability = -1; + i < 1000 && (iClassification < 0 || iProbability < 0); + i++) + { + ErrorCode = Parameters.FindString ("attr:name", i, &StringPntr); + if (ErrorCode != B_OK) + break; /* Reached the end of the attributes. */ + if (strcmp (StringPntr, g_AttributeNameClassification) == 0) + iClassification = i; + else if (strcmp (StringPntr, g_AttributeNameSpamRatio) == 0) + iProbability = i; + } + + /* Add extra default settings for those programs which previously didn't + update the MIME database with all the attributes that exist (so our new + additions don't show up at the wrong index). */ + + i--; /* Set i to index of last valid attribute. */ + + for (j = 0; j <= i; j++) + { + if (Parameters.FindString ("attr:public_name", j, &StringPntr) == + B_BAD_INDEX) + { + if (Parameters.FindString ("attr:name", j, &StringPntr) != B_OK) + StringPntr = "None!"; + Parameters.AddString ("attr:public_name", StringPntr); + } + } + + while (Parameters.FindInt32 ("attr:type", i, &TempInt32) == B_BAD_INDEX) + Parameters.AddInt32 ("attr:type", B_STRING_TYPE); + + while (Parameters.FindBool ("attr:viewable", i, &TempBool) == B_BAD_INDEX) + Parameters.AddBool ("attr:viewable", true); + + while (Parameters.FindBool ("attr:editable", i, &TempBool) == B_BAD_INDEX) + Parameters.AddBool ("attr:editable", false); + + while (Parameters.FindInt32 ("attr:width", i, &TempInt32) == B_BAD_INDEX) + Parameters.AddInt32 ("attr:width", 60); + + while (Parameters.FindInt32 ("attr:alignment", i, &TempInt32) == B_BAD_INDEX) + Parameters.AddInt32 ("attr:alignment", B_ALIGN_LEFT); + + while (Parameters.FindBool ("attr:extra", i, &TempBool) == B_BAD_INDEX) + Parameters.AddBool ("attr:extra", false); + + /* Add our new attributes to e-mail related things, if not already there. */ + + if (iClassification < 0) + { + Parameters.AddString ("attr:name", g_AttributeNameClassification); + Parameters.AddString ("attr:public_name", "Classification Group"); + Parameters.AddInt32 ("attr:type", B_STRING_TYPE); + Parameters.AddBool ("attr:viewable", true); + Parameters.AddBool ("attr:editable", false); + Parameters.AddInt32 ("attr:width", 45); + Parameters.AddInt32 ("attr:alignment", B_ALIGN_LEFT); + Parameters.AddBool ("attr:extra", false); + } + + if (iProbability < 0) + { + Parameters.AddString ("attr:name", g_AttributeNameSpamRatio); + Parameters.AddString ("attr:public_name", "Spam/Genuine Estimate"); + Parameters.AddInt32 ("attr:type", B_FLOAT_TYPE); + Parameters.AddBool ("attr:viewable", true); + Parameters.AddBool ("attr:editable", false); + Parameters.AddInt32 ("attr:width", 50); + Parameters.AddInt32 ("attr:alignment", B_ALIGN_LEFT); + Parameters.AddBool ("attr:extra", false); + } + + if (iClassification < 0 || iProbability < 0) + { + ErrorCode = MimeType.SetAttrInfo (&Parameters); + if (ErrorCode != B_OK) + { + sprintf (ErrorMessage, "Unable to associate the classification " + "attributes with e-mail messages in the MIME database"); + return ErrorCode; + } + } + + /* Set up the MIME type for the database file. */ + + sprintf (ErrorMessage, "Problems with setting up MIME type (%s) for " + "the database files", g_ABSDatabaseFileMIMEType); /* A generic message. */ + + ErrorCode = MimeType.SetTo (g_ABSDatabaseFileMIMEType); + if (ErrorCode != B_OK) + return ErrorCode; + + MimeType.Delete (); + ErrorCode = MimeType.Install (); + if (ErrorCode != B_OK) + { + sprintf (ErrorMessage, "Failed to install MIME type (%s) in the system", + MimeType.Type ()); + return ErrorCode; + } + + MimeType.SetShortDescription ("Spam Database"); + MimeType.SetLongDescription ("Bayesian Statistical Database for " + "Classifying Junk E-Mail"); + sprintf (ErrorMessage, "1.0 ('%s')", g_DatabaseRecognitionString); + MimeType.SetSnifferRule (ErrorMessage); + MimeType.SetPreferredApp (g_ABSAppSignature); + + /* Set up the names of the sound effects. Later on the user can associate + sound files with the names by using the Sounds preferences panel or the + installsound command. The MDR add-on filter will trigger these sounds. */ + + add_system_beep_event (g_BeepGenuine); + add_system_beep_event (g_BeepSpam); + add_system_beep_event (g_BeepUncertain); + + return B_OK; +} + + +/* Load the database if it hasn't been loaded yet. Otherwise do nothing. */ + +status_t ABSApp::LoadDatabaseIfNeeded (char *ErrorMessage) +{ + if (m_WordMap.empty ()) + return LoadSaveDatabase (true /* DoLoad */, ErrorMessage); + + return B_OK; +} + + +/* Either load the database of spam words (DoLoad is TRUE) from the file +specified in the settings, or write (DoLoad is FALSE) the database to it. If +it doesn't exist (and its parent directories do exist) then it will be created +when saving. If it doesn't exist when loading, the in-memory database will be +set to an empty one and an error will be returned with an explanation put into +ErrorMessage (should be big enough for a path name and a couple of lines of +text). + +The database file format is a UTF-8 text file (well, there could be some +latin-1 characters and other junk in there - it just copies the bytes from the +e-mail messages directly), with tab characters to separate fields (so that you +can also load it into a spreadsheet). The first line identifies the overall +file type. The second lists pairs of classifications plus the number of +messages in each class. Currently it is just Genuine and Spam, but for future +compatability, that could be followed by more classification pairs. The +remaining lines each contain a word, the date it was last updated (actually +it's the number of messages in the database when the word was added, smaller +numbers mean it was updated longer ago), the genuine count and the spam count. +*/ + +status_t ABSApp::LoadSaveDatabase (bool DoLoad, char *ErrorMessage) +{ + time_t CurrentTime; + FILE *DatabaseFile = NULL; + BNode DatabaseNode; + BNodeInfo DatabaseNodeInfo; + StatisticsMap::iterator DataIter; + StatisticsMap::iterator EndIter; + status_t ErrorCode; + int i; + pair InsertResult; + char LineString [10240]; + StatisticsRecord Statistics; + const char *StringPntr; + char *TabPntr; + const char *WordPntr; + + if (DoLoad) + { + MakeDatabaseEmpty (); + m_DatabaseHasChanged = false; /* In case of early error exit. */ + } + else /* Saving the database, backup the old version on disk. */ + { + ErrorCode = MakeBackup (ErrorMessage); + if (ErrorCode != B_OK) /* Usually because the directory isn't there. */ + return ErrorCode; + } + + DatabaseFile = fopen (m_DatabaseFileName.String (), DoLoad ? "rb" : "wb"); + if (DatabaseFile == NULL) + { + ErrorCode = errno; + sprintf (ErrorMessage, "Can't open database file \"%s\" for %s", + m_DatabaseFileName.String (), DoLoad ? "reading" : "writing"); + goto ErrorExit; + } + + /* Process the first line, which identifies the file. */ + + if (DoLoad) + { + sprintf (ErrorMessage, "Can't read first line of database file \"%s\", " + "expected it to start with \"%s\"", + m_DatabaseFileName.String (), g_DatabaseRecognitionString); + ErrorCode = -1; + + if (fgets (LineString, sizeof (LineString), DatabaseFile) == NULL) + goto ErrorExit; + if (strncmp (LineString, g_DatabaseRecognitionString, + strlen (g_DatabaseRecognitionString)) != 0) + goto ErrorExit; + } + else /* Saving */ + { + CurrentTime = time (NULL); + if (fprintf (DatabaseFile, "%s V1 (word, age, genuine count, spam count)\t" + "Written by AGMSBayesianSpamServer $Revision: 1.1 $\t" + "Compiled on " __DATE__ " at " __TIME__ "\tThis file saved on %s", + g_DatabaseRecognitionString, ctime (&CurrentTime)) <= 0) + { + ErrorCode = errno; + sprintf (ErrorMessage, "Problems when writing to database file \"%s\"", + m_DatabaseFileName.String ()); + goto ErrorExit; + } + } + + /* The second line lists the different classifications. We just check to see + that the first two are Genuine and Spam. If there are others, they'll be + ignored and lost when the database is saved. */ + + if (DoLoad) + { + sprintf (ErrorMessage, "Can't read second line of database file \"%s\", " + "expected it to list classifications %s and %s along with their totals", + m_DatabaseFileName.String (), g_ClassifiedGenuine, g_ClassifiedSpam); + ErrorCode = B_BAD_VALUE; + + if (fgets (LineString, sizeof (LineString), DatabaseFile) == NULL) + goto ErrorExit; + i = strlen (LineString); + if (i > 0 && LineString[i-1] == '\n') + LineString[i-1] = 0; /* Remove trailing line feed character. */ + + /* Look for the title word at the start of the line. */ + + TabPntr = LineString; + for (StringPntr = TabPntr; *TabPntr != 0 && *TabPntr != '\t'; TabPntr++) + ; if (*TabPntr == '\t') *TabPntr++ = 0; /* Stringify up to next tab. */ + + if (strncmp (StringPntr, "Classifications", 15) != 0) + goto ErrorExit; + + /* Look for the Genuine class and count. */ + + for (StringPntr = TabPntr; *TabPntr != 0 && *TabPntr != '\t'; TabPntr++) + ; if (*TabPntr == '\t') *TabPntr++ = 0; /* Stringify up to next tab. */ + + if (strcmp (StringPntr, g_ClassifiedGenuine) != 0) + goto ErrorExit; + + for (StringPntr = TabPntr; *TabPntr != 0 && *TabPntr != '\t'; TabPntr++) + ; if (*TabPntr == '\t') *TabPntr++ = 0; /* Stringify up to next tab. */ + + m_TotalGenuineMessages = atoll (StringPntr); + + /* Look for the Spam class and count. */ + + for (StringPntr = TabPntr; *TabPntr != 0 && *TabPntr != '\t'; TabPntr++) + ; if (*TabPntr == '\t') *TabPntr++ = 0; /* Stringify up to next tab. */ + + if (strcmp (StringPntr, g_ClassifiedSpam) != 0) + goto ErrorExit; + + for (StringPntr = TabPntr; *TabPntr != 0 && *TabPntr != '\t'; TabPntr++) + ; if (*TabPntr == '\t') *TabPntr++ = 0; /* Stringify up to next tab. */ + + m_TotalSpamMessages = atoll (StringPntr); + } + else /* Saving */ + { + fprintf (DatabaseFile, + "Classifications and total messages:\t%s\t%lu\t%s\t%lu\n", + g_ClassifiedGenuine, m_TotalGenuineMessages, + g_ClassifiedSpam, m_TotalSpamMessages); + } + + /* The remainder of the file is the list of words and statistics. Each line + has a word, a tab, the time when the word was last changed in the database + (sequence number of message addition, starts at 0 and goes up by one for each + message added to the database), a tab then the number of messages in the + first class (genuine) that had that word, then a tab, then the number of + messages in the second class (spam) with that word, and so on. */ + + if (DoLoad) + { + while (!feof (DatabaseFile)) + { + if (fgets (LineString, sizeof (LineString), DatabaseFile) == NULL) + { + ErrorCode = errno; + if (feof (DatabaseFile)) + break; + if (ErrorCode == B_OK) + ErrorCode = -1; + sprintf (ErrorMessage, "Error while reading words and statistics " + "from database file \"%s\"", m_DatabaseFileName.String ()); + goto ErrorExit; + } + + i = strlen (LineString); + if (i > 0 && LineString[i-1] == '\n') + LineString[i-1] = 0; /* Remove trailing line feed character. */ + + /* Get the word at the start of the line, save in WordPntr. */ + + TabPntr = LineString; + for (WordPntr = TabPntr; *TabPntr != 0 && *TabPntr != '\t'; TabPntr++) + ; if (*TabPntr == '\t') *TabPntr++ = 0; /* Stringify up to next tab. */ + + /* Get the date stamp. Actually a sequence number, not a date. */ + + for (StringPntr = TabPntr; *TabPntr != 0 && *TabPntr != '\t'; TabPntr++) + ; if (*TabPntr == '\t') *TabPntr++ = 0; /* Stringify up to next tab. */ + + Statistics.age = atoll (StringPntr); + + /* Get the Genuine count. */ + + for (StringPntr = TabPntr; *TabPntr != 0 && *TabPntr != '\t'; TabPntr++) + ; if (*TabPntr == '\t') *TabPntr++ = 0; /* Stringify up to next tab. */ + + Statistics.genuineCount = atoll (StringPntr); + + /* Get the Spam count. */ + + for (StringPntr = TabPntr; *TabPntr != 0 && *TabPntr != '\t'; TabPntr++) + ; if (*TabPntr == '\t') *TabPntr++ = 0; /* Stringify up to next tab. */ + + Statistics.spamCount = atoll (StringPntr); + + /* Ignore empty words, totally unused words and ones which are too long + (avoids lots of length checking everywhere). */ + + if (WordPntr[0] == 0 || strlen (WordPntr) > g_MaxWordLength || + (Statistics.genuineCount <= 0 && Statistics.spamCount <= 0)) + continue; /* Ignore this line of text, start on next one. */ + + /* Add the combination to the database. */ + + InsertResult = m_WordMap.insert ( + StatisticsMap::value_type (WordPntr, Statistics)); + if (InsertResult.second == false) + { + ErrorCode = B_BAD_VALUE; + sprintf (ErrorMessage, "Error while inserting word \"%s\" from " + "database \"%s\", perhaps it is a duplicate", + WordPntr, m_DatabaseFileName.String ()); + goto ErrorExit; + } + m_WordCount++; + + /* And the hunt for the oldest word. */ + + if (Statistics.age < m_OldestAge) + m_OldestAge = Statistics.age; + } + } + else /* Saving, dump all words and statistics to the file. */ + { + EndIter = m_WordMap.end (); + for (DataIter = m_WordMap.begin (); DataIter != EndIter; DataIter++) + { + if (fprintf (DatabaseFile, "%s\t%lu\t%lu\t%lu\n", + DataIter->first.c_str (), DataIter->second.age, + DataIter->second.genuineCount, DataIter->second.spamCount) <= 0) + { + ErrorCode = errno; + sprintf (ErrorMessage, "Error while writing word \"%s\" to " + "database \"%s\"", + DataIter->first.c_str(), m_DatabaseFileName.String ()); + goto ErrorExit; + } + } + } + + /* Set the file type so that the new file gets associated with this program, + and picks up the right icon. */ + + if (!DoLoad) + { + sprintf (ErrorMessage, "Unable to set attributes (file type) of database " + "file \"%s\"", m_DatabaseFileName.String ()); + ErrorCode = DatabaseNode.SetTo (m_DatabaseFileName.String ()); + if (ErrorCode != B_OK) + goto ErrorExit; + DatabaseNodeInfo.SetTo (&DatabaseNode); + ErrorCode = DatabaseNodeInfo.SetType (g_ABSDatabaseFileMIMEType); + if (ErrorCode != B_OK) + goto ErrorExit; + } + + /* Success! */ + m_DatabaseHasChanged = false; + ErrorCode = B_OK; + +ErrorExit: + if (DatabaseFile != NULL) + fclose (DatabaseFile); + return ErrorCode; +} + + +/* Either load the settings (DoLoad is TRUE) from the configuration file or +write them (DoLoad is FALSE) to it. The configuration file is a flattened +BMessage containing the various program settings. If it doesn't exist (and its +parent directories don't exist) then it will be created when saving. If it +doesn't exist when loading, the settings will be set to default values. */ + +status_t ABSApp::LoadSaveSettings (bool DoLoad) +{ + status_t ErrorCode; + const char *NamePntr; + BMessage Settings; + BDirectory SettingsDirectory; + BFile SettingsFile; + const char *StringPntr; + bool TempBool; + int32 TempInt32; + char TempString [PATH_MAX + 100]; + + /* Preset things to default values if loading, in case of an error or it's an + older version of the settings file which doesn't have every field defined. */ + + if (DoLoad) + DefaultSettings (); + + /* Look for our settings directory. When saving we can try to create it. */ + + ErrorCode = SettingsDirectory.SetTo (m_SettingsDirectoryPath.Path ()); + if (ErrorCode != B_OK) + { + if (DoLoad || ErrorCode != B_ENTRY_NOT_FOUND) + { + sprintf (TempString, "Can't find settings directory \"%s\"", + m_SettingsDirectoryPath.Path ()); + goto ErrorExit; + } + ErrorCode = create_directory (m_SettingsDirectoryPath.Path (), 0755); + if (ErrorCode == B_OK) + ErrorCode = SettingsDirectory.SetTo (m_SettingsDirectoryPath.Path ()); + if (ErrorCode != B_OK) + { + sprintf (TempString, "Can't create settings directory \"%s\"", + m_SettingsDirectoryPath.Path ()); + goto ErrorExit; + } + } + + ErrorCode = SettingsFile.SetTo (&SettingsDirectory, g_SettingsFileName, + DoLoad ? B_READ_ONLY : B_READ_WRITE | B_CREATE_FILE | B_ERASE_FILE); + if (ErrorCode != B_OK) + { + sprintf (TempString, "Can't open settings file \"%s\" in directory \"%s\" " + "for %s", g_SettingsFileName, m_SettingsDirectoryPath.Path(), + DoLoad ? "reading" : "writing"); + goto ErrorExit; + } + + if (DoLoad) + { + ErrorCode = Settings.Unflatten (&SettingsFile); + if (ErrorCode != 0 || Settings.what != g_SettingsWhatCode) + { + sprintf (TempString, "Corrupt data detected while reading settings " + "file \"%s\" in directory \"%s\", will revert to defaults", + g_SettingsFileName, m_SettingsDirectoryPath.Path()); + goto ErrorExit; + } + } + + /* Transfer the settings between the BMessage and our various global + variables. For loading, if the setting isn't present, leave it at the + default value. Note that loading and saving are intermingled here to make + code maintenance easier (less chance of forgetting to update it if load and + save were separate functions). */ + + ErrorCode = B_OK; /* So that saving settings can record an error. */ + + NamePntr = "DatabaseFileName"; + if (DoLoad) + { + if (Settings.FindString (NamePntr, &StringPntr) == B_OK) + m_DatabaseFileName.SetTo (StringPntr); + } + else if (ErrorCode == B_OK) + ErrorCode = Settings.AddString (NamePntr, m_DatabaseFileName); + + NamePntr = "ServerMode"; + if (DoLoad) + { + if (Settings.FindBool (NamePntr, &TempBool) == B_OK) + g_ServerMode = TempBool; + } + else if (ErrorCode == B_OK) + ErrorCode = Settings.AddBool (NamePntr, g_ServerMode); + + NamePntr = "IgnorePreviousClassification"; + if (DoLoad) + { + if (Settings.FindBool (NamePntr, &TempBool) == B_OK) + m_IgnorePreviousClassification = TempBool; + } + else if (ErrorCode == B_OK) + ErrorCode = Settings.AddBool (NamePntr, m_IgnorePreviousClassification); + + NamePntr = "PurgeAge"; + if (DoLoad) + { + if (Settings.FindInt32 (NamePntr, &TempInt32) == B_OK) + m_PurgeAge = TempInt32; + } + else if (ErrorCode == B_OK) + ErrorCode = Settings.AddInt32 (NamePntr, m_PurgeAge); + + NamePntr = "PurgePopularity"; + if (DoLoad) + { + if (Settings.FindInt32 (NamePntr, &TempInt32) == B_OK) + m_PurgePopularity = TempInt32; + } + else if (ErrorCode == B_OK) + ErrorCode = Settings.AddInt32 (NamePntr, m_PurgePopularity); + + NamePntr = "ScoringMode"; + if (DoLoad) + { + if (Settings.FindInt32 (NamePntr, &TempInt32) == B_OK) + m_ScoringMode = (ScoringModes) TempInt32; + if (m_ScoringMode < 0 || m_ScoringMode >= SM_MAX) + m_ScoringMode = (ScoringModes) 0; + } + else if (ErrorCode == B_OK) + ErrorCode = Settings.AddInt32 (NamePntr, m_ScoringMode); + + NamePntr = "TokenizeMode"; + if (DoLoad) + { + if (Settings.FindInt32 (NamePntr, &TempInt32) == B_OK) + m_TokenizeMode = (TokenizeModes) TempInt32; + if (m_TokenizeMode < 0 || m_TokenizeMode >= TM_MAX) + m_TokenizeMode = (TokenizeModes) 0; + } + else if (ErrorCode == B_OK) + ErrorCode = Settings.AddInt32 (NamePntr, m_TokenizeMode); + + if (ErrorCode != B_OK) + { + strcpy (TempString, "Unable to stuff the program settings into a " + "temporary BMessage, settings not saved"); + goto ErrorExit; + } + + /* Save the settings BMessage to the settings file. */ + + if (!DoLoad) + { + Settings.what = g_SettingsWhatCode; + ErrorCode = Settings.Flatten (&SettingsFile); + if (ErrorCode != 0) + { + sprintf (TempString, "Problems while writing settings file \"%s\" in " + "directory \"%s\"", g_SettingsFileName, + m_SettingsDirectoryPath.Path ()); + goto ErrorExit; + } + } + + m_SettingsHaveChanged = false; + return B_OK; + +ErrorExit: /* Error message in TempString, code in ErrorCode. */ + DisplayErrorMessage (TempString, ErrorCode, DoLoad ? + "Loading Settings Error" : "Saving Settings Error"); + return ErrorCode; +} + + +void ABSApp::MessageReceived (BMessage *MessagePntr) +{ + const char *PropertyName; + struct property_info *PropInfoPntr; + int32 SpecifierIndex; + int32 SpecifierKind; + BMessage SpecifierMessage; + + /* See if it is a scripting message that applies to the database or one of + the other operations this program supports. Pass on other scripting messages + to the inherited parent MessageReceived function (they're usually scripting + messages for the BApplication). */ + + switch (MessagePntr->what) + { + case B_GET_PROPERTY: + case B_SET_PROPERTY: + case B_COUNT_PROPERTIES: + case B_CREATE_PROPERTY: + case B_DELETE_PROPERTY: + case B_EXECUTE_PROPERTY: + if (MessagePntr->GetCurrentSpecifier (&SpecifierIndex, &SpecifierMessage, + &SpecifierKind, &PropertyName) == B_OK && + SpecifierKind == B_DIRECT_SPECIFIER) + { + for (PropInfoPntr = g_ScriptingPropertyList + 0; true; PropInfoPntr++) + { + if (PropInfoPntr->name == 0) + break; /* Ran out of commands. */ + + if (PropInfoPntr->commands[0] == MessagePntr->what && + strcasecmp (PropInfoPntr->name, PropertyName) == 0) + { + ProcessScriptingMessage (MessagePntr, PropInfoPntr); + return; + } + } + } + break; + } + + /* Pass the unprocessed message to the inherited function, maybe it knows + what to do. This includes replies to messages we sent ourselves. */ + + BApplication::MessageReceived (MessagePntr); +} + + +/* Rename the existing database file to a backup file name, potentially +replacing an older backup. If something goes wrong, returns an error code and +puts an explanation in ErrorMessage. */ + +status_t ABSApp::MakeBackup (char *ErrorMessage) +{ + BEntry Entry; + status_t ErrorCode; + int i; + char LeafName [NAME_MAX]; + char NewName [PATH_MAX+20]; + char OldName [PATH_MAX+20]; + + ErrorCode = Entry.SetTo (m_DatabaseFileName.String ()); + if (ErrorCode != B_OK) + { + sprintf (ErrorMessage, "While making backup, failed to make a BEntry for " + "\"%s\" (maybe the directory doesn't exist?)", + m_DatabaseFileName.String ()); + return ErrorCode; + } + if (!Entry.Exists ()) + return B_OK; /* No existing file to worry about overwriting. */ + Entry.GetName (LeafName); + + /* Find the first hole (no file) where we will stop the renaming chain. */ + + for (i = 0; i < g_MaxBackups - 1; i++) + { + strcpy (OldName, m_DatabaseFileName.String ()); + sprintf (OldName + strlen (OldName), g_BackupSuffix, i); + Entry.SetTo (OldName); + if (!Entry.Exists ()) + break; + } + + /* Move the files down by one to fill in the hole in the name series. */ + + for (i--; i >= 0; i--) + { + strcpy (OldName, m_DatabaseFileName.String ()); + sprintf (OldName + strlen (OldName), g_BackupSuffix, i); + Entry.SetTo (OldName); + strcpy (NewName, LeafName); + sprintf (NewName + strlen (NewName), g_BackupSuffix, i + 1); + ErrorCode = Entry.Rename (NewName, true /* clobber */); + } + + Entry.SetTo (m_DatabaseFileName.String ()); + strcpy (NewName, LeafName); + sprintf (NewName + strlen (NewName), g_BackupSuffix, 0); + ErrorCode = Entry.Rename (NewName, true /* clobber */); + if (ErrorCode != B_OK) + sprintf (ErrorMessage, "While making backup, failed to rename " + "\"%s\" to \"%s\"", m_DatabaseFileName.String (), NewName); + + return ErrorCode; +} + + +void ABSApp::MakeDatabaseEmpty () +{ + m_WordMap.clear (); /* Sets the map to empty, deallocating any old data. */ + m_WordCount = 0; + m_TotalGenuineMessages = 0; + m_TotalSpamMessages = 0; + m_OldestAge = (uint32) -1 /* makes largest number possible */; +} + + +/* Do what the scripting command says. A reply message will be sent back with +several fields: "error" containing the numerical error code (0 for success), +"CommandText" with a text representation of the command, "result" with the +resulting data for a get or count command. If it isn't understood, then rather +than a B_REPLY kind of message, it will be a B_MESSAGE_NOT_UNDERSTOOD message +with an "error" number and an "message" string with a description. */ + +void ABSApp::ProcessScriptingMessage ( + BMessage *MessagePntr, + struct property_info *PropInfoPntr) +{ + bool ArgumentBool = false; + bool ArgumentGotBool = false; + bool ArgumentGotInt32 = false; + bool ArgumentGotString = false; + int32 ArgumentInt32 = 0; + const char *ArgumentString = NULL; + BString CommandText; + status_t ErrorCode; + int i; + BMessage ReplyMessage (B_MESSAGE_NOT_UNDERSTOOD); + ssize_t StringBufferSize; + BMessage TempBMessage; + BPath TempPath; + char TempString [PATH_MAX + 1024]; + + if (g_QuitCountdown >= 0 && !g_CommandLineMode) + { + g_QuitCountdown = -1; + cerr << "Quit countdown aborted due to a scripting command arriving.\n"; + } + + ErrorCode = MessagePntr->FindData (g_DataName, B_STRING_TYPE, + (const void **) &ArgumentString, &StringBufferSize); + if (ErrorCode == B_OK) + { + if (PropInfoPntr->extra_data != PN_EVALUATE_STRING && + PropInfoPntr->extra_data != PN_SPAM_STRING && + PropInfoPntr->extra_data != PN_GENUINE_STRING && + strlen (ArgumentString) >= PATH_MAX) + { + sprintf (TempString, "\"data\" string of a scripting message is too " + "long, for SET %s action", PropInfoPntr->name); + ErrorCode = B_NAME_TOO_LONG; + goto ErrorExit; + } + ArgumentGotString = true; + } + else if (MessagePntr->FindBool (g_DataName, &ArgumentBool) == B_OK) + ArgumentGotBool = true; + else if (MessagePntr->FindInt32 (g_DataName, &ArgumentInt32) == B_OK) + ArgumentGotInt32 = true; + + /* Prepare a Human readable description of the scripting command. */ + + switch (PropInfoPntr->commands[0]) + { + case B_SET_PROPERTY: + CommandText.SetTo ("Set "); + break; + + case B_GET_PROPERTY: + CommandText.SetTo ("Get "); + break; + + case B_COUNT_PROPERTIES: + CommandText.SetTo ("Count "); + break; + + case B_CREATE_PROPERTY: + CommandText.SetTo ("Create "); + break; + + case B_DELETE_PROPERTY: + CommandText.SetTo ("Delete "); + break; + + case B_EXECUTE_PROPERTY: + CommandText.SetTo ("Execute "); + break; + + default: + sprintf (TempString, "Bug: scripting command for \"%s\" has an unknown " + "action code %d", PropInfoPntr->name, + (int) PropInfoPntr->commands[0]); + ErrorCode = -1; + goto ErrorExit; + } + CommandText.Append (PropInfoPntr->name); + + /* Add on the argument value to our readable command, if there is one. */ + + if (ArgumentGotString) + { + CommandText.Append (" \""); + CommandText.Append (ArgumentString); + CommandText.Append ("\""); + } + if (ArgumentGotBool) + CommandText.Append (ArgumentBool ? " true" : " false"); + if (ArgumentGotInt32) + { + sprintf (TempString, " %ld", ArgumentInt32); + CommandText.Append (TempString); + } + + /* From now on the scripting command has been recognized and is in the + correct format, so it always returns a B_REPLY message. A readable version + of the command is also added to make debugging easier. */ + + ReplyMessage.what = B_REPLY; + ReplyMessage.AddString ("CommandText", CommandText); + + /* Now actually do the command. First prepare a default error message. */ + + sprintf (TempString, "Operation code %d (get, set, count, etc) " + "unsupported for property %s", + (int) PropInfoPntr->commands[0], PropInfoPntr->name); + ErrorCode = B_BAD_INDEX; + + switch (PropInfoPntr->extra_data) + { + case PN_DATABASE_FILE: + switch (PropInfoPntr->commands[0]) + { + case B_GET_PROPERTY: /* Get the database file name. */ + ReplyMessage.AddString (g_ResultName, m_DatabaseFileName); + break; + + case B_SET_PROPERTY: /* Set the database file name to a new one. */ + if (!ArgumentGotString) + { + ErrorCode = B_BAD_TYPE; + sprintf (TempString, "You need to specify a string for the " + "SET %s command", PropInfoPntr->name); + goto ErrorExit; + } + ErrorCode = TempPath.SetTo (ArgumentString, NULL /* leaf */, + true /* normalize - verifies parent directories exist */); + if (ErrorCode != B_OK) + { + sprintf (TempString, "New database path name of \"%s\" is invalid " + "(parent directories must exist)", ArgumentString); + goto ErrorExit; + } + if ((ErrorCode = SaveDatabaseIfNeeded (TempString)) != B_OK) + goto ErrorExit; + MakeDatabaseEmpty (); /* So that the new one gets loaded if used. */ + + if (strlen (TempPath.Leaf ()) > NAME_MAX-strlen(g_BackupSuffix)-1) + { + /* Truncate the name so that there is enough space for the backup + extension. Approximately. */ + strcpy (TempString, TempPath.Leaf ()); + TempString [NAME_MAX - strlen (g_BackupSuffix) - 1] = 0; + TempPath.GetParent (&TempPath); + TempPath.Append (TempString); + } + m_DatabaseFileName.SetTo (TempPath.Path ()); + m_SettingsHaveChanged = true; + break; + + case B_CREATE_PROPERTY: /* Make a new database file plus more. */ + if ((ErrorCode = CreateDatabaseFile (TempString)) != B_OK) + goto ErrorExit; + break; + + case B_DELETE_PROPERTY: /* Delete the file and its backups too. */ + if ((ErrorCode = DeleteDatabaseFile (TempString)) != B_OK) + goto ErrorExit; + break; + + case B_COUNT_PROPERTIES: + if ((ErrorCode = LoadDatabaseIfNeeded (TempString)) != B_OK) + goto ErrorExit; + ReplyMessage.AddInt32 (g_ResultName, m_WordCount); + break; + + default: /* Unknown operation code, error message already set. */ + goto ErrorExit; + } + break; + + case PN_SPAM: + case PN_SPAM_STRING: + case PN_GENUINE: + case PN_GENUINE_STRING: + case PN_UNCERTAIN: + switch (PropInfoPntr->commands[0]) + { + case B_COUNT_PROPERTIES: /* Get the number of spam/genuine messages. */ + if ((ErrorCode = LoadDatabaseIfNeeded (TempString)) != B_OK) + goto ErrorExit; + if (PropInfoPntr->extra_data == PN_SPAM || + PropInfoPntr->extra_data == PN_SPAM_STRING) + ReplyMessage.AddInt32 (g_ResultName, m_TotalSpamMessages); + else + ReplyMessage.AddInt32 (g_ResultName, m_TotalGenuineMessages); + break; + + case B_SET_PROPERTY: /* Add spam/genuine/uncertain to database. */ + if (!ArgumentGotString) + { + ErrorCode = B_BAD_TYPE; + sprintf (TempString, "You need to specify a string (%s) " + "for the SET %s command", + (PropInfoPntr->extra_data == PN_GENUINE_STRING || + PropInfoPntr->extra_data == PN_SPAM_STRING) + ? "text of the message to be added" + : "pathname of the file containing the text to be added", + PropInfoPntr->name); + goto ErrorExit; + } + if ((ErrorCode = LoadDatabaseIfNeeded (TempString)) != B_OK) + goto ErrorExit; + if (PropInfoPntr->extra_data == PN_GENUINE || + PropInfoPntr->extra_data == PN_SPAM || + PropInfoPntr->extra_data == PN_UNCERTAIN) + ErrorCode = AddFileToDatabase ( + (PropInfoPntr->extra_data == PN_SPAM) ? CL_SPAM : + ((PropInfoPntr->extra_data == PN_GENUINE) ? CL_GENUINE : + CL_UNCERTAIN), + ArgumentString, TempString /* ErrorMessage */); + else + ErrorCode = AddStringToDatabase ( + (PropInfoPntr->extra_data == PN_SPAM_STRING) ? + CL_SPAM : CL_GENUINE, + ArgumentString, TempString /* ErrorMessage */); + if (ErrorCode != B_OK) + goto ErrorExit; + break; + + default: /* Unknown operation code, error message already set. */ + goto ErrorExit; + } + break; + + case PN_IGNORE_PREVIOUS_CLASSIFICATION: + switch (PropInfoPntr->commands[0]) + { + case B_GET_PROPERTY: + ReplyMessage.AddBool (g_ResultName, m_IgnorePreviousClassification); + break; + + case B_SET_PROPERTY: + if (!ArgumentGotBool) + { + ErrorCode = B_BAD_TYPE; + sprintf (TempString, "You need to specify a boolean (true/yes, " + "false/no) for the SET %s command", PropInfoPntr->name); + goto ErrorExit; + } + m_IgnorePreviousClassification = ArgumentBool; + m_SettingsHaveChanged = true; + break; + + default: /* Unknown operation code, error message already set. */ + goto ErrorExit; + } + break; + + case PN_SERVER_MODE: + switch (PropInfoPntr->commands[0]) + { + case B_GET_PROPERTY: + ReplyMessage.AddBool (g_ResultName, g_ServerMode); + break; + + case B_SET_PROPERTY: + if (!ArgumentGotBool) + { + ErrorCode = B_BAD_TYPE; + sprintf (TempString, "You need to specify a boolean (true/yes, " + "false/no) for the SET %s command", PropInfoPntr->name); + goto ErrorExit; + } + g_ServerMode = ArgumentBool; + m_SettingsHaveChanged = true; + break; + + default: /* Unknown operation code, error message already set. */ + goto ErrorExit; + } + break; + + case PN_FLUSH: + if (PropInfoPntr->commands[0] == B_EXECUTE_PROPERTY && + (ErrorCode = SaveDatabaseIfNeeded (TempString)) == B_OK) + break; + goto ErrorExit; + + case PN_PURGE_AGE: + switch (PropInfoPntr->commands[0]) + { + case B_GET_PROPERTY: + ReplyMessage.AddInt32 (g_ResultName, m_PurgeAge); + break; + + case B_SET_PROPERTY: + if (!ArgumentGotInt32) + { + ErrorCode = B_BAD_TYPE; + sprintf (TempString, "You need to specify a 32 bit integer " + "for the SET %s command", PropInfoPntr->name); + goto ErrorExit; + } + m_PurgeAge = ArgumentInt32; + m_SettingsHaveChanged = true; + break; + + default: /* Unknown operation code, error message already set. */ + goto ErrorExit; + } + break; + + case PN_PURGE_POPULARITY: + switch (PropInfoPntr->commands[0]) + { + case B_GET_PROPERTY: + ReplyMessage.AddInt32 (g_ResultName, m_PurgePopularity); + break; + + case B_SET_PROPERTY: + if (!ArgumentGotInt32) + { + ErrorCode = B_BAD_TYPE; + sprintf (TempString, "You need to specify a 32 bit integer " + "for the SET %s command", PropInfoPntr->name); + goto ErrorExit; + } + m_PurgePopularity = ArgumentInt32; + m_SettingsHaveChanged = true; + break; + + default: /* Unknown operation code, error message already set. */ + goto ErrorExit; + } + break; + + case PN_PURGE: + if (PropInfoPntr->commands[0] == B_EXECUTE_PROPERTY && + (ErrorCode = LoadDatabaseIfNeeded (TempString)) == B_OK && + (ErrorCode = PurgeOldWords (TempString)) == B_OK) + break; + goto ErrorExit; + + case PN_OLDEST: + if (PropInfoPntr->commands[0] == B_GET_PROPERTY && + (ErrorCode = LoadDatabaseIfNeeded (TempString)) == B_OK) + { + ReplyMessage.AddInt32 (g_ResultName, m_OldestAge); + break; + } + goto ErrorExit; + + case PN_EVALUATE: + case PN_EVALUATE_STRING: + if (PropInfoPntr->commands[0] == B_SET_PROPERTY) + { + if (!ArgumentGotString) + { + ErrorCode = B_BAD_TYPE; + sprintf (TempString, "You need to specify a string for the " + "SET %s command", PropInfoPntr->name); + goto ErrorExit; + } + if ((ErrorCode = LoadDatabaseIfNeeded (TempString)) == B_OK) + { + if (PropInfoPntr->extra_data == PN_EVALUATE) + { + if ((ErrorCode = EvaluateFile (ArgumentString, &ReplyMessage, + TempString)) == B_OK) + break; + } + else /* PN_EVALUATE_STRING */ + { + if ((ErrorCode = EvaluateString (ArgumentString, StringBufferSize, + &ReplyMessage, TempString)) == B_OK) + break; + } + } + } + goto ErrorExit; + + case PN_RESET_TO_DEFAULTS: + if (PropInfoPntr->commands[0] == B_EXECUTE_PROPERTY) + { + DefaultSettings (); + break; + } + goto ErrorExit; + + case PN_INSTALL_THINGS: + if (PropInfoPntr->commands[0] == B_EXECUTE_PROPERTY && + (ErrorCode = InstallThings (TempString)) == B_OK) + break; + goto ErrorExit; + + case PN_SCORING_MODE: + switch (PropInfoPntr->commands[0]) + { + case B_GET_PROPERTY: + ReplyMessage.AddString (g_ResultName, + g_ScoringModeNames[m_ScoringMode]); + break; + + case B_SET_PROPERTY: + i = SM_MAX; + if (ArgumentGotString) + for (i = 0; i < SM_MAX; i++) + { + if (strcasecmp (ArgumentString, g_ScoringModeNames [i]) == 0) + { + m_ScoringMode = (ScoringModes) i; + m_SettingsHaveChanged = true; + break; + } + } + if (i >= SM_MAX) /* Didn't find a valid scoring mode word. */ + { + ErrorCode = B_BAD_TYPE; + sprintf (TempString, "You used the unrecognized \"%s\" as " + "a scoring mode for the SET %s command. Should be one of: ", + ArgumentGotString ? ArgumentString : "not specified", + PropInfoPntr->name); + for (i = 0; i < SM_MAX; i++) + { + strcat (TempString, g_ScoringModeNames [i]); + if (i < SM_MAX - 1) + strcat (TempString, ", "); + } + goto ErrorExit; + } + break; + + default: /* Unknown operation code, error message already set. */ + goto ErrorExit; + } + break; + + case PN_TOKENIZE_MODE: + switch (PropInfoPntr->commands[0]) + { + case B_GET_PROPERTY: + ReplyMessage.AddString (g_ResultName, + g_TokenizeModeNames[m_TokenizeMode]); + break; + + case B_SET_PROPERTY: + i = TM_MAX; + if (ArgumentGotString) + for (i = 0; i < TM_MAX; i++) + { + if (strcasecmp (ArgumentString, g_TokenizeModeNames [i]) == 0) + { + m_TokenizeMode = (TokenizeModes) i; + m_SettingsHaveChanged = true; + break; + } + } + if (i >= TM_MAX) /* Didn't find a valid tokenize mode word. */ + { + ErrorCode = B_BAD_TYPE; + sprintf (TempString, "You used the unrecognized \"%s\" as " + "a tokenize mode for the SET %s command. Should be one of: ", + ArgumentGotString ? ArgumentString : "not specified", + PropInfoPntr->name); + for (i = 0; i < TM_MAX; i++) + { + strcat (TempString, g_TokenizeModeNames [i]); + if (i < TM_MAX - 1) + strcat (TempString, ", "); + } + goto ErrorExit; + } + break; + + default: /* Unknown operation code, error message already set. */ + goto ErrorExit; + } + break; + + default: + sprintf (TempString, "Bug! Unrecognized property identification " + "number %d (should be between 0 and %d). Fix the entry in " + "the g_ScriptingPropertyList array!", + (int) PropInfoPntr->extra_data, PN_MAX - 1); + goto ErrorExit; + } + + /* Success. */ + + ReplyMessage.AddInt32 ("error", B_OK); + ErrorCode = MessagePntr->SendReply (&ReplyMessage, + this /* Reply's reply handler */, 500000 /* send timeout */); + if (ErrorCode != B_OK) + cerr << "ProcessScriptingMessage failed to send a reply message, code " << + ErrorCode << " (" << strerror (ErrorCode) << ")" << " for " << + CommandText.String () << endl; + return; + +ErrorExit: /* Error message in TempString, return code in ErrorCode. */ + ReplyMessage.AddInt32 ("error", ErrorCode); + ReplyMessage.AddString ("message", TempString); + DisplayErrorMessage (TempString, ErrorCode); + ErrorCode = MessagePntr->SendReply (&ReplyMessage, + this /* Reply's reply handler */, 500000 /* send timeout */); + if (ErrorCode != B_OK) + cerr << "ProcessScriptingMessage failed to send an error message, code " << + ErrorCode << " (" << strerror (ErrorCode) << ")" << " for " << + CommandText.String () << endl; +} + + +/* Since quitting stops the program before the results of a script command are +received, we use a time delay to do the quit and make sure there are no pending +commands being processed by the auxiliary looper which is sending us commands. +Also, we have a countdown which can be interrupted by an incoming scripting +message in case one client tells us to quit while another one is still using us +(happens when you have two or more e-mail accounts). But if the system is +shutting down, quit immediately! */ + +void ABSApp::Pulse () +{ + if (g_QuitCountdown == 0) + { + if (g_CommanderLooperPntr == NULL || + !g_CommanderLooperPntr->IsBusy ()) + PostMessage (B_QUIT_REQUESTED); + } + else if (g_QuitCountdown > 0) + { + cerr << "AGMSBayesianSpamServer quitting in " << g_QuitCountdown << ".\n"; + g_QuitCountdown--; + } +} + + +/* A quit request message has come in. If the quit countdown has reached zero, +allow the request, otherwise reject it (and start the countdown if it hasn't +been started). */ + +bool ABSApp::QuitRequested () +{ + BMessage *QuitMessage; + team_info RemoteInfo; + BMessenger RemoteMessenger; + team_id RemoteTeam; + + /* See if the quit is from the system shutdown command (which goes through + the registrar server), if so, quit immediately. */ + + QuitMessage = CurrentMessage (); + if (QuitMessage != NULL && QuitMessage->IsSourceRemote ()) + { + RemoteMessenger = QuitMessage->ReturnAddress (); + RemoteTeam = RemoteMessenger.Team (); + if (get_team_info (RemoteTeam, &RemoteInfo) == B_OK && + strstr (RemoteInfo.args, "registrar") != NULL) + g_QuitCountdown = 0; + } + + if (g_QuitCountdown == 0) + return BApplication::QuitRequested (); + + if (g_QuitCountdown < 0) + g_QuitCountdown = 10; /* Start the countdown. */ + + return false; +} + + +/* Go through the current database and delete words which are too old (time is +equivalent to the number of messages added to the database) and too unpopular +(words not used by many messages). Hopefully this will get rid of words which +are just hunks of binary or other garbage. The database has been loaded +elsewhere. */ + +status_t ABSApp::PurgeOldWords (char *ErrorMessage) +{ + uint32 CurrentTime; + StatisticsMap::iterator CurrentIter; + StatisticsMap::iterator EndIter; + StatisticsMap::iterator NextIter; + char TempString [80]; + + strcpy (ErrorMessage, "Purge can't fail"); /* So argument gets used. */ + CurrentTime = m_TotalGenuineMessages + m_TotalSpamMessages - 1; + m_OldestAge = (uint32) -1 /* makes largest number possible */; + + EndIter = m_WordMap.end (); + NextIter = m_WordMap.begin (); + while (NextIter != EndIter) + { + CurrentIter = NextIter++; + + if (CurrentTime - CurrentIter->second.age >= m_PurgeAge && + CurrentIter->second.genuineCount + CurrentIter->second.spamCount <= + m_PurgePopularity) + { + /* Delete this word, it is unpopular and old. Sob. */ + + m_WordMap.erase (CurrentIter); + if (m_WordCount > 0) + m_WordCount--; + + m_DatabaseHasChanged = true; + } + else /* This word is still in the database. Update oldest age. */ + { + if (CurrentIter->second.age < m_OldestAge) + m_OldestAge = CurrentIter->second.age; + } + } + + /* Just a little bug check here. Just in case. */ + + if (m_WordCount != m_WordMap.size ()) + { + sprintf (TempString, "Our word count of %lu doesn't match the " + "size of the database, %lu", m_WordCount, m_WordMap.size ()); + DisplayErrorMessage (TempString, -1, "Bug!"); + m_WordCount = m_WordMap.size (); + } + + return B_OK; +} + + +void ABSApp::ReadyToRun () +{ + DatabaseWindow *DatabaseWindowPntr; + float JunkFloat; + BButton *TempButtonPntr; + BCheckBox *TempCheckBoxPntr; + font_height TempFontHeight; + BMenuBar *TempMenuBarPntr; + BMenuItem *TempMenuItemPntr; + BPopUpMenu *TempPopUpMenuPntr; + BRadioButton *TempRadioButtonPntr; + BRect TempRect; + const char *TempString = "Testing My Things"; + BStringView *TempStringViewPntr; + BTextControl *TempTextPntr; + BWindow *TempWindowPntr; + + /* This batch of code gets some measurements which will be used for laying + out controls and other GUI elements. Set the spacing between buttons and + other controls to the width of the letter "M" in the user's desired font. */ + + g_MarginBetweenControls = (int) be_plain_font->StringWidth ("M"); + + /* Also find out how much space a line of text uses. */ + + be_plain_font->GetHeight (&TempFontHeight); + g_LineOfTextHeight = ceilf ( + TempFontHeight.ascent + TempFontHeight.descent + TempFontHeight.leading); + + /* Start finding out the height of various user interface gadgets, which can + vary based on the current font size. Make a temporary gadget, which is + attached to our window, then resize it to its prefered size so that it + accomodates the font size and other frills it needs. */ + + TempWindowPntr = new BWindow (BRect (10, 20, 200, 200), "Temporary Window", + B_DOCUMENT_WINDOW, B_NO_WORKSPACE_ACTIVATION | B_ASYNCHRONOUS_CONTROLS); + if (TempWindowPntr == NULL) + { + DisplayErrorMessage ("Unable to create temporary window for finding " + "sizes of controls."); + g_QuitCountdown = 0; + return; + } + + TempRect = TempWindowPntr->Bounds (); + + /* Find the height of a single line of text in a BStringView. */ + + TempStringViewPntr = new BStringView (TempRect, TempString, TempString); + if (TempStringViewPntr != NULL) + { + TempWindowPntr->Lock (); + TempWindowPntr->AddChild (TempStringViewPntr); + TempStringViewPntr->GetPreferredSize (&JunkFloat, &g_StringViewHeight); + TempWindowPntr->RemoveChild (TempStringViewPntr); + TempWindowPntr->Unlock (); + delete TempStringViewPntr; + } + + /* Find the height of a button, which seems to be larger than a text + control and can make life difficult. Make a temporary button, which + is attached to our window so that it resizes to accomodate the font size. */ + + TempButtonPntr = new BButton (TempRect, TempString, TempString, NULL); + if (TempButtonPntr != NULL) + { + TempWindowPntr->Lock (); + TempWindowPntr->AddChild (TempButtonPntr); + TempButtonPntr->GetPreferredSize (&JunkFloat, &g_ButtonHeight); + TempWindowPntr->RemoveChild (TempButtonPntr); + TempWindowPntr->Unlock (); + delete TempButtonPntr; + } + + /* Find the height of a text box. */ + + TempTextPntr = new BTextControl (TempRect, TempString, NULL /* label */, + TempString, NULL); + if (TempTextPntr != NULL) + { + TempWindowPntr->Lock (); + TempWindowPntr->AddChild (TempTextPntr); + TempTextPntr->GetPreferredSize (&JunkFloat, &g_TextBoxHeight); + TempWindowPntr->RemoveChild (TempTextPntr); + TempWindowPntr->Unlock (); + delete TempTextPntr; + } + + /* Find the height of a checkbox control. */ + + TempCheckBoxPntr = new BCheckBox (TempRect, TempString, TempString, NULL); + if (TempCheckBoxPntr != NULL) + { + TempWindowPntr->Lock (); + TempWindowPntr->AddChild (TempCheckBoxPntr); + TempCheckBoxPntr->GetPreferredSize (&JunkFloat, &g_CheckBoxHeight); + TempWindowPntr->RemoveChild (TempCheckBoxPntr); + TempWindowPntr->Unlock (); + delete TempCheckBoxPntr; + } + + /* Find the height of a radio button control. */ + + TempRadioButtonPntr = + new BRadioButton (TempRect, TempString, TempString, NULL); + if (TempRadioButtonPntr != NULL) + { + TempWindowPntr->Lock (); + TempWindowPntr->AddChild (TempRadioButtonPntr); + TempRadioButtonPntr->GetPreferredSize (&JunkFloat, &g_RadioButtonHeight); + TempWindowPntr->RemoveChild (TempRadioButtonPntr); + TempWindowPntr->Unlock (); + delete TempRadioButtonPntr; + } + + /* Find the height of a pop-up menu. */ + + TempMenuBarPntr = new BMenuBar (TempRect, TempString, + B_FOLLOW_LEFT | B_FOLLOW_TOP, B_ITEMS_IN_COLUMN, + true /* resize to fit items */); + TempPopUpMenuPntr = new BPopUpMenu (TempString); + TempMenuItemPntr = new BMenuItem (TempString, new BMessage (12345), 'g'); + + if (TempMenuBarPntr != NULL && TempPopUpMenuPntr != NULL && + TempMenuItemPntr != NULL) + { + TempPopUpMenuPntr->AddItem (TempMenuItemPntr); + TempMenuBarPntr->AddItem (TempPopUpMenuPntr); + + TempWindowPntr->Lock (); + TempWindowPntr->AddChild (TempMenuBarPntr); + TempMenuBarPntr->GetPreferredSize (&JunkFloat, &g_PopUpMenuHeight); + TempWindowPntr->RemoveChild (TempMenuBarPntr); + TempWindowPntr->Unlock (); + delete TempMenuBarPntr; // It will delete contents too. + } + + TempWindowPntr->Lock (); + TempWindowPntr->Quit (); + + SetPulseRate (500000); + + if (g_CommandLineMode) + g_QuitCountdown = 0; /* Quit as soon as queued up commands done. */ + else /* GUI mode, make a window. */ + { + DatabaseWindowPntr = new DatabaseWindow (); + if (DatabaseWindowPntr == NULL) + { + DisplayErrorMessage ("Unable to create window."); + g_QuitCountdown = 0; + } + else + DatabaseWindowPntr->Show (); /* Starts the window's message loop. */ + } + g_AppReadyToRunCompleted = true; +} + + +/* Given a mail component (body text, attachment, whatever), look for words in +it. If the tokenize mode specifies that it isn't one of the ones we are +looking for, just skip it. For container type components, recursively examine +their contents. */ + +status_t ABSApp::RecursivelyTokenizeMailComponent ( + BMailComponent *ComponentPntr, + const char *OptionalFileName, + set &WordSet, + char *ErrorMessage, + int RecursionLevel) +{ + char AttachmentName [B_FILE_NAME_LENGTH]; + BMailAttachment *AttachmentPntr; + BMimeType ComponentMIMEType; + BMailContainer *ContainerPntr; + BMallocIO ContentsIO; + const char *ContentsBufferPntr; + size_t ContentsBufferSize; + status_t ErrorCode; + bool ExamineComponent; + const char *HeaderKeyPntr; + const char *HeaderValuePntr; + int i; + int j; + const char *NameExtension; + int NumComponents; + BMimeType TextAnyMIMEType ("text"); + BMimeType TextPlainMIMEType ("text/plain"); + + if (ComponentPntr == NULL) + return B_OK; + + /* Add things in the sub-headers that might be useful. Things like the file + name of attachments, the encoding type, etc. */ + + if (m_TokenizeMode == TM_PLAIN_TEXT_HEADER || + m_TokenizeMode == TM_ANY_TEXT_HEADER || + m_TokenizeMode == TM_ALL_PARTS_HEADER || + m_TokenizeMode == TM_JUST_HEADER) + { + for (i = 0; i < 1000; i++) + { + HeaderKeyPntr = ComponentPntr->HeaderAt (i); + if (HeaderKeyPntr == NULL) + break; + AddWordsToSet (HeaderKeyPntr, strlen (HeaderKeyPntr), WordSet); + for (j = 0; j < 1000; j++) + { + HeaderValuePntr = ComponentPntr->HeaderField (HeaderKeyPntr, j); + if (HeaderValuePntr == NULL) + break; + AddWordsToSet (HeaderValuePntr, strlen (HeaderValuePntr), WordSet); + } + } + } + + /* Check the MIME type of the thing. It's used to decide if the contents are + worth examining for words. */ + + ErrorCode = ComponentPntr->MIMEType (&ComponentMIMEType); + if (ErrorCode != B_OK) + { + sprintf (ErrorMessage, "ABSApp::RecursivelyTokenizeMailComponent: " + "Unable to get MIME type at level %d in \"%s\"", + RecursionLevel, OptionalFileName); + return ErrorCode; + } + if (ComponentMIMEType.Type() == NULL) + { + /* Have to make up a MIME type for things which don't have them, such as + the main body text, otherwise it would get ignored. */ + + if (NULL != dynamic_cast(ComponentPntr)) + ComponentMIMEType.SetType ("text/plain"); + } + if (!TextAnyMIMEType.Contains (&ComponentMIMEType) && + NULL != (AttachmentPntr = + dynamic_cast(ComponentPntr))) + { + /* Sometimes spam doesn't give a text MIME type for text when they do an + attachment (which is often base64 encoded). Use the file name extension to + see if it really is text. */ + NameExtension = NULL; + if (AttachmentPntr->FileName (AttachmentName) >= 0) + NameExtension = strrchr (AttachmentName, '.'); + if (NameExtension != NULL) + { + if (strcasecmp (NameExtension, ".txt") == 0) + ComponentMIMEType.SetType ("text/plain"); + else if (strcasecmp (NameExtension, ".htm") == 0 || + strcasecmp (NameExtension, ".html") == 0) + ComponentMIMEType.SetType ("text/html"); + } + } + + switch (m_TokenizeMode) + { + case TM_PLAIN_TEXT: + case TM_PLAIN_TEXT_HEADER: + ExamineComponent = TextPlainMIMEType.Contains (&ComponentMIMEType); + break; + + case TM_ANY_TEXT: + case TM_ANY_TEXT_HEADER: + ExamineComponent = TextAnyMIMEType.Contains (&ComponentMIMEType); + break; + + case TM_ALL_PARTS: + case TM_ALL_PARTS_HEADER: + ExamineComponent = true; + break; + + default: + ExamineComponent = false; + break; + } + + if (ExamineComponent) + { + /* Get the contents of the component. This will be UTF-8 text (converted + from whatever encoding was used) for text attachments. For other ones, + it's just the raw data, or perhaps decoded from base64 encoding. */ + + ContentsIO.SetBlockSize (16 * 1024); + ErrorCode = ComponentPntr->GetDecodedData (&ContentsIO); + if (ErrorCode == B_OK) /* Can fail for container components: no data. */ + { + /* Look for words in the decoded data. */ + + ContentsBufferPntr = (const char *) ContentsIO.Buffer (); + ContentsBufferSize = ContentsIO.BufferLength (); + if (ContentsBufferPntr != NULL /* can be empty */) + AddWordsToSet (ContentsBufferPntr, ContentsBufferSize, WordSet); + } + } + + /* Examine any sub-components in the message. */ + + if (NULL != (ContainerPntr = + dynamic_cast(ComponentPntr))) + { + NumComponents = ContainerPntr->CountComponents (); + + for (i = 0; i < NumComponents; i++) + { + ComponentPntr = ContainerPntr->GetComponent (i); + + ErrorCode = RecursivelyTokenizeMailComponent (ComponentPntr, + OptionalFileName, WordSet, ErrorMessage, RecursionLevel + 1); + if (ErrorCode != B_OK) + break; + } + } + + return ErrorCode; +} + + +/* The user has tried to open a file or several files with this application, +via Tracker's open-with menu item. If it is a database type file, then change +the database file name to it. Otherwise, ask the user whether they want to +classify it as spam or non-spam. There will be at most around 100 files, BeOS +R5.0.3's Tracker crashes if it tries to pass on more than that many using Open +With... etc. The command is sent to an intermediary thread where it is +asynchronously converted into a scripting message(s) that are sent back to this +BApplication. The intermediary is needed since we can't recursively execute +scripting messages while processing a message (this RefsReceived one). */ + +void ABSApp::RefsReceived (BMessage *MessagePntr) +{ + if (g_CommanderLooperPntr != NULL) + g_CommanderLooperPntr->CommandReferences (MessagePntr); +} + + +/* A scripting command is looking for something to execute it. See if it is +targetted at our database. */ + +BHandler * ABSApp::ResolveSpecifier ( + BMessage *MessagePntr, + int32 Index, + BMessage *SpecifierMsgPntr, + int32 SpecificationKind, + const char *PropertyPntr) +{ + int i; + + /* See if it is one of our commands. */ + + if (SpecificationKind == B_DIRECT_SPECIFIER) + { + for (i = PN_MAX - 1; i >= 0; i--) + { + if (strcasecmp (PropertyPntr, g_PropertyNames [i]) == 0) + return this; /* Found it! Return the Handler (which is us). */ + } + } + + /* Handle an unrecognized scripting command, let the parent figure it out. */ + + return BApplication::ResolveSpecifier ( + MessagePntr, Index, SpecifierMsgPntr, SpecificationKind, PropertyPntr); +} + + +/* Save the database if it hasn't been saved yet. Otherwise do nothing. */ + +status_t ABSApp::SaveDatabaseIfNeeded (char *ErrorMessage) +{ + if (m_DatabaseHasChanged) + return LoadSaveDatabase (false /* DoLoad */, ErrorMessage); + + return B_OK; +} + + +/* Presumably the file is an e-mail message (or at least the header portion of +one). Break it into parts: header, body and MIME components. Then add the +words in the portions that match the current tokenization settings to the set +of words. */ + +status_t ABSApp::TokenizeParts ( + BPositionIO *PositionIOPntr, + const char *OptionalFileName, + set &WordSet, + char *ErrorMessage) +{ + status_t ErrorCode = B_OK; + BMailComponent HeaderComponent; + const char *HeaderKeyPntr; + const char *HeaderValuePntr; + int i; + int j; + BEmailMessage WholeEMail; + + sprintf (ErrorMessage, "ABSApp::TokenizeParts: While getting e-mail " + "headers, had problems with \"%s\"", OptionalFileName); + + /* Add the words in the header, if requested. */ + + if (m_TokenizeMode == TM_PLAIN_TEXT_HEADER || + m_TokenizeMode == TM_ANY_TEXT_HEADER || + m_TokenizeMode == TM_ALL_PARTS_HEADER || + m_TokenizeMode == TM_JUST_HEADER) + { + /* Load the message into a plain BMailComponent so that the headers are + decoded but not otherwise modified (the BEmailMessage class deletes most + of them so we can't reuse it). */ + + ErrorCode = PositionIOPntr->Seek (0, SEEK_SET); + if (ErrorCode < 0) goto ErrorExit; + ErrorCode = HeaderComponent.SetToRFC822 (PositionIOPntr, + -1 /* length */, true /* parse_now */); + if (ErrorCode < 0) goto ErrorExit; + for (i = 0; i < 1000; i++) + { + HeaderKeyPntr = HeaderComponent.HeaderAt (i); + if (HeaderKeyPntr == NULL) + break; + AddWordsToSet (HeaderKeyPntr, strlen (HeaderKeyPntr), WordSet); + for (j = 0; j < 1000; j++) + { + HeaderValuePntr = HeaderComponent.HeaderField (HeaderKeyPntr, j); + if (HeaderValuePntr == NULL) + break; + AddWordsToSet (HeaderValuePntr, strlen (HeaderValuePntr), WordSet); + } + } + } + + if (m_TokenizeMode != TM_JUST_HEADER) + { + ErrorCode = WholeEMail.SetToRFC822 ( + PositionIOPntr /* it does its own seeking to the start */, + -1 /* length */, true /* parse_now */); + if (ErrorCode < 0) goto ErrorExit; + + ErrorCode = RecursivelyTokenizeMailComponent (&WholeEMail, + OptionalFileName, WordSet, ErrorMessage, 0 /* RecursionLevel */); + } + +ErrorExit: + return ErrorCode; +} + + +/* Add all the words in the whole file or memory buffer to the supplied set. +The file doesn't have to be an e-mail message since it isn't parsed for e-mail +headers or MIME headers or anything. It blindly adds everything that looks +like a word, though it does convert quoted printable codes to the characters +they represent. See also AddWordsToSet which does something similar. */ + +status_t ABSApp::TokenizeWhole ( + BPositionIO *PositionIOPntr, + const char *OptionalFileName, + set &WordSet, + char *ErrorMessage) +{ + string AccumulatedWord; + uint8 Buffer [16 * 1024]; + uint8 *BufferCurrentPntr = Buffer + 0; + uint8 *BufferEndPntr = Buffer + 0; + const char *IOErrorString = + "TokenizeWhole: Error %ld while reading \"%s\""; + size_t Length; + int Letter = ' '; + char HexString [4]; + int NextLetter = ' '; + int NextNextLetter = ' '; + + /* Use a buffer since reading single characters from a BFile is so slow. + BufferCurrentPntr is the position of the next character to be read. When it + reaches BufferEndPntr, it is time to fill the buffer again. */ + +#define ReadChar(CharVar) \ + { \ + if (BufferCurrentPntr < BufferEndPntr) \ + CharVar = *BufferCurrentPntr++; \ + else /* Try to fill the buffer. */ \ + { \ + ssize_t AmountRead; \ + AmountRead = PositionIOPntr->Read (Buffer, sizeof (Buffer)); \ + if (AmountRead < 0) \ + { \ + sprintf (ErrorMessage, IOErrorString, AmountRead, OptionalFileName); \ + return AmountRead; \ + } \ + else if (AmountRead == 0) \ + CharVar = EOF; \ + else \ + { \ + BufferEndPntr = Buffer + AmountRead; \ + BufferCurrentPntr = Buffer + 0; \ + CharVar = *BufferCurrentPntr++; \ + } \ + } \ + } + + /* Read all the words in the file and add them to our local set of words. A + set is used since we don't care how many times a word occurs. */ + + while (true) + { + /* We read two letters ahead so that we can decode quoted printable + characters (an equals sign followed by two hex digits or a new line). Note + that Letter can become EOF (-1) when end of file is reached. */ + + Letter = NextLetter; + NextLetter = NextNextLetter; + ReadChar (NextNextLetter); + + /* Decode quoted printable codes first, so that the rest of the code just + sees an ordinary character. Or even nothing, if it is the hidden line + break combination. This may falsely corrupt stuff following an equals + sign, but usually won't. */ + + if (Letter == '=') + { + if ((NextLetter == '\r' && NextNextLetter == '\n') || + (NextLetter == '\n' && NextNextLetter == '\r')) + { + /* Make the "=\r\n" pair disappear. It's not even white space. */ + ReadChar (NextLetter); + ReadChar (NextNextLetter); + continue; + } + if (NextLetter == '\n' || NextLetter == '\r') + { + /* Make the "=\n" pair disappear. It's not even white space. */ + NextLetter = NextNextLetter; + ReadChar (NextNextLetter); + continue; + } + if (NextNextLetter != EOF && + isxdigit (NextLetter) && isxdigit (NextNextLetter)) + { + /* Convert the hex code to a letter. */ + HexString[0] = NextLetter; + HexString[1] = NextNextLetter; + HexString[2] = 0; + Letter = strtoul (HexString, NULL, 16 /* number system base */); + ReadChar (NextLetter); + ReadChar (NextNextLetter); + } + } + + /* Convert to lower case to improve word matches. Of course this loses a + bit of information, such as MONEY vs Money, an indicator of spam. Well, + apparently that is actually a useful distinction, so leave the case alone. + if (Letter >= 0 && Letter < 128) + Letter = tolower (Letter); + */ + + /* See if it is a letter we treat as white space - all control characters + and all punctuation except for: apostrophe (so "it's" and possessive + versions of words get stored), dash (for hyphenated words), dollar sign + (for cash amounts), period (for IP addresses, we later remove trailing + (periods). Note that codes above 127 are UTF-8 characters, which we + consider non-space. */ + + if (Letter < 0 /* EOF */ || (Letter < 128 && m_SpaceCharacters[Letter])) + { + /* That space finished off a word. Remove trailing periods... */ + + while ((Length = AccumulatedWord.size()) > 0 && + AccumulatedWord [Length-1] == '.') + AccumulatedWord.resize (Length - 1); + + /* If there's anything left in the word, add it to the set. Also ignore + words which are too big (it's probably some binary encoded data). But + leave room for supercalifragilisticexpialidoceous. According to one web + site, pneumonoultramicroscopicsilicovolcanoconiosis is the longest word + currently in English. Note that some uuencoded data was seen with a 60 + character line length. */ + + if (Length > 0 && Length <= g_MaxWordLength) + WordSet.insert (AccumulatedWord); + + /* Empty out the string to get ready for the next word. */ + + AccumulatedWord.resize (0); + } + else /* Not a space-like character, add it to the word. */ + AccumulatedWord.append (1 /* one copy of the char */, (char) Letter); + + /* Stop at end of file or error. Don't care which. Exit here so that last + word got processed. */ + + if (Letter == EOF) + break; + } + + return B_OK; +} + + + +/****************************************************************************** + * Implementation of the ClassificationChoicesView class, constructor, + * destructor and the rest of the member functions in mostly alphabetical + * order. + */ + +ClassificationChoicesWindow::ClassificationChoicesWindow ( + BRect FrameRect, + const char *FileName, + int NumberOfFiles) +: BWindow (FrameRect, "Classification Choices", B_TITLED_WINDOW, + B_NOT_ZOOMABLE | B_NOT_RESIZABLE | B_ASYNCHRONOUS_CONTROLS), + m_BulkModeSelectedPntr (NULL), + m_ChoosenClassificationPntr (NULL) +{ + ClassificationChoicesView *SubViewPntr; + + SubViewPntr = new ClassificationChoicesView (Bounds(), + FileName, NumberOfFiles); + AddChild (SubViewPntr); + SubViewPntr->ResizeToPreferred (); + ResizeTo (SubViewPntr->Frame().Width(), SubViewPntr->Frame().Height()); +} + + +void ClassificationChoicesWindow::MessageReceived (BMessage *MessagePntr) +{ + BControl *ControlPntr; + + if (MessagePntr->what >= MSG_CLASS_BUTTONS && + MessagePntr->what < MSG_CLASS_BUTTONS + CL_MAX) + { + if (m_ChoosenClassificationPntr != NULL) + *m_ChoosenClassificationPntr = + (ClassificationTypes) (MessagePntr->what - MSG_CLASS_BUTTONS); + PostMessage (B_QUIT_REQUESTED); // Close and destroy the window. + return; + } + + if (MessagePntr->what == MSG_BULK_CHECKBOX) + { + if (m_BulkModeSelectedPntr != NULL && + MessagePntr->FindPointer ("source", (void **) &ControlPntr) == B_OK) + *m_BulkModeSelectedPntr = (ControlPntr->Value() == B_CONTROL_ON); + return; + } + + if (MessagePntr->what == MSG_CANCEL_BUTTON) + { + PostMessage (B_QUIT_REQUESTED); // Close and destroy the window. + return; + } + + BWindow::MessageReceived (MessagePntr); +} + + +void ClassificationChoicesWindow::Go ( + bool *BulkModeSelectedPntr, + ClassificationTypes *ChoosenClassificationPntr) +{ + status_t ErrorCode = 0; + BView *MainViewPntr; + thread_id WindowThreadID; + + m_BulkModeSelectedPntr = BulkModeSelectedPntr; + m_ChoosenClassificationPntr = ChoosenClassificationPntr; + if (m_ChoosenClassificationPntr != NULL) + *m_ChoosenClassificationPntr = CL_MAX; + + Show (); // Starts the window thread running. + + /* Move the window to the center of the screen it is now being displayed on + (have to wait for it to be showing). */ + + Lock (); + MainViewPntr = FindView ("ClassificationChoicesView"); + if (MainViewPntr != NULL) + { + BRect TempRect; + BScreen TempScreen (this); + float X; + float Y; + + TempRect = TempScreen.Frame (); + X = TempRect.Width() / 2; + Y = TempRect.Height() / 2; + TempRect = MainViewPntr->Frame(); + X -= TempRect.Width() / 2; + Y -= TempRect.Height() / 2; + MoveTo (ceilf (X), ceilf (Y)); + } + Unlock (); + + /* Wait for the window to go away. */ + + WindowThreadID = Thread (); + if (WindowThreadID >= 0) + // Delay until the window thread has died, presumably window deleted now. + wait_for_thread (WindowThreadID, &ErrorCode); +} + + + +/****************************************************************************** + * Implementation of the ClassificationChoicesView class, constructor, + * destructor and the rest of the member functions in mostly alphabetical + * order. + */ + +ClassificationChoicesView::ClassificationChoicesView ( + BRect FrameRect, + const char *FileName, + int NumberOfFiles) +: BView (FrameRect, "ClassificationChoicesView", + B_FOLLOW_TOP | B_FOLLOW_LEFT, B_WILL_DRAW | B_NAVIGABLE_JUMP), + m_FileName (FileName), + m_NumberOfFiles (NumberOfFiles), + m_PreferredBottomY (ceilf (g_ButtonHeight * 10)) +{ +} + + +void ClassificationChoicesView::AttachedToWindow () +{ + BButton *ButtonPntr; + BCheckBox *CheckBoxPntr; + ClassificationTypes Classification; + float Margin; + float RowHeight; + float RowTop; + BTextView *TextViewPntr; + BRect TempRect; + char TempString [2048]; + BRect TextRect; + float X; + + SetViewColor (ui_color (B_PANEL_BACKGROUND_COLOR)); + + RowHeight = g_ButtonHeight; + if (g_CheckBoxHeight > RowHeight) + RowHeight = g_CheckBoxHeight; + RowHeight = ceilf (RowHeight * 1.1); + + TempRect = Bounds (); + RowTop = TempRect.top; + + /* Show the file name text. */ + + Margin = ceilf ((RowHeight - g_StringViewHeight) / 2); + TempRect = Bounds (); + TempRect.top = RowTop + Margin; + TextRect = TempRect; + TextRect.OffsetTo (0, 0); + TextRect.InsetBy (g_MarginBetweenControls, 2); + sprintf (TempString, "How do you want to classify the file named \"%s\"?", + m_FileName); + TextViewPntr = new BTextView (TempRect, "FileText", TextRect, + B_FOLLOW_TOP | B_FOLLOW_LEFT, B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE); + AddChild (TextViewPntr); + TextViewPntr->SetText (TempString); + TextViewPntr->MakeEditable (false); + TextViewPntr->SetViewColor (ui_color (B_PANEL_BACKGROUND_COLOR)); + TextViewPntr->ResizeTo (TempRect.Width (), + 3 + TextViewPntr->TextHeight (0, sizeof (TempString))); + RowTop = TextViewPntr->Frame().bottom + Margin; + + /* Make the classification buttons. */ + + Margin = ceilf ((RowHeight - g_ButtonHeight) / 2); + TempRect = Bounds (); + TempRect.top = RowTop + Margin; + X = Bounds().left + g_MarginBetweenControls; + for (Classification = (ClassificationTypes) 0; Classification < CL_MAX; + Classification = (ClassificationTypes) ((int) Classification + 1)) + { + TempRect = Bounds (); + TempRect.top = RowTop + Margin; + TempRect.left = X; + sprintf (TempString, "%s Button", + g_ClassificationTypeNames [Classification]); + ButtonPntr = new BButton (TempRect, TempString, + g_ClassificationTypeNames [Classification], new BMessage ( + ClassificationChoicesWindow::MSG_CLASS_BUTTONS + Classification)); + AddChild (ButtonPntr); + ButtonPntr->ResizeToPreferred (); + X = ButtonPntr->Frame().right + 3 * g_MarginBetweenControls; + } + RowTop += ceilf (RowHeight * 1.2); + + /* Make the Cancel button. */ + + Margin = ceilf ((RowHeight - g_ButtonHeight) / 2); + TempRect = Bounds (); + TempRect.top = RowTop + Margin; + TempRect.left += g_MarginBetweenControls; + ButtonPntr = new BButton (TempRect, "Cancel Button", + "Cancel", new BMessage (ClassificationChoicesWindow::MSG_CANCEL_BUTTON)); + AddChild (ButtonPntr); + ButtonPntr->ResizeToPreferred (); + X = ButtonPntr->Frame().right + g_MarginBetweenControls; + + /* Make the checkbox for bulk operations. */ + + if (m_NumberOfFiles > 1) + { + Margin = ceilf ((RowHeight - g_CheckBoxHeight) / 2); + TempRect = Bounds (); + TempRect.top = RowTop + Margin; + TempRect.left = X; + sprintf (TempString, "Mark all %d remaining messages the same way.", + m_NumberOfFiles - 1); + CheckBoxPntr = new BCheckBox (TempRect, "BulkBox", TempString, + new BMessage (ClassificationChoicesWindow::MSG_BULK_CHECKBOX)); + AddChild (CheckBoxPntr); + CheckBoxPntr->ResizeToPreferred (); + } + RowTop += RowHeight; + + m_PreferredBottomY = RowTop; +} + + +void ClassificationChoicesView::GetPreferredSize (float *width, float *height) +{ + if (width != NULL) + *width = Bounds().Width(); + if (height != NULL) + *height = m_PreferredBottomY; +} + + + +/****************************************************************************** + * Implementation of the CommanderLooper class, constructor, destructor and the + * rest of the member functions in mostly alphabetical order. + */ + +CommanderLooper::CommanderLooper () +: BLooper ("CommanderLooper", B_NORMAL_PRIORITY), + m_IsBusy (false) +{ +} + + +CommanderLooper::~CommanderLooper () +{ + g_CommanderLooperPntr = NULL; + delete g_CommanderMessenger; + g_CommanderMessenger = NULL; +} + + +/* Process some command line arguments. Basically just send a message to this +looper itself to do the work later. That way the caller can continue doing +whatever they're doing, particularly if it's the BApplication. */ + +void CommanderLooper::CommandArguments (int argc, char **argv) +{ + int i; + BMessage InternalMessage; + + InternalMessage.what = MSG_COMMAND_ARGUMENTS; + for (i = 0; i < argc; i++) + InternalMessage.AddString ("arg", argv[i]); + + PostMessage (&InternalMessage); +} + + +/* Copy the refs out of the given message and stuff them into an internal +message to ourself (so that the original message can be returned to the caller, +and if it is Tracker, it can close the file handles it has open). Optionally +allow preset classification rather than asking the user (set BulkMode to TRUE +and specify the class with BulkClassification). */ + +void CommanderLooper::CommandReferences ( + BMessage *MessagePntr, + bool BulkMode, + ClassificationTypes BulkClassification) +{ + entry_ref EntryRef; + int i; + BMessage InternalMessage; + + InternalMessage.what = MSG_COMMAND_FILE_REFS; + for (i = 0; MessagePntr->FindRef ("refs", i, &EntryRef) == B_OK; i++) + InternalMessage.AddRef ("refs", &EntryRef); + InternalMessage.AddBool ("BulkMode", BulkMode); + InternalMessage.AddInt32 ("BulkClassification", BulkClassification); + + PostMessage (&InternalMessage); +} + + +/* This function is called by other threads to see if the CommanderLooper is +busy working on something. */ + +bool CommanderLooper::IsBusy () +{ + if (m_IsBusy) + return true; + + if (IsLocked () || !MessageQueue()->IsEmpty ()) + return true; + + return false; +} + + +void CommanderLooper::MessageReceived (BMessage *MessagePntr) +{ + m_IsBusy = true; + + if (MessagePntr->what == MSG_COMMAND_ARGUMENTS) + ProcessArgs (MessagePntr); + else if (MessagePntr->what == MSG_COMMAND_FILE_REFS) + ProcessRefs (MessagePntr); + else + BLooper::MessageReceived (MessagePntr); + + m_IsBusy = false; +} + + +/* Process the command line by converting it into a series of scripting +messages (possibly thousands) and sent them to the BApplication synchronously +(so we can print the result). */ + +void CommanderLooper::ProcessArgs (BMessage *MessagePntr) +{ + int32 argc = 0; + const char **argv = NULL; + int ArgumentIndex; + uint32 CommandCode; + const char *CommandWord; + status_t ErrorCode; + const char *ErrorTitle = "ProcessArgs"; + char *EndPntr; + int32 i; + BMessage ReplyMessage; + BMessage ScriptMessage; + struct property_info *PropInfoPntr; + const char *PropertyName; + bool TempBool; + float TempFloat; + int32 TempInt32; + const char *TempStringPntr; + type_code TypeCode; + const char *ValuePntr; + + /* Get the argument count and pointers to arguments out of the message and + into our argc and argv. */ + + ErrorCode = MessagePntr->GetInfo ("arg", &TypeCode, &argc); + if (ErrorCode != B_OK || TypeCode != B_STRING_TYPE) + { + DisplayErrorMessage ("Unable to find argument strings in message", + ErrorCode, ErrorTitle); + goto ErrorExit; + } + + if (argc < 2) + { + cerr << PrintUsage; + DisplayErrorMessage ("You need to specify a command word, like GET, SET " + "and so on followed by a property, like DatabaseFile, and maybe " + "followed by a value of some sort", -1, ErrorTitle); + goto ErrorExit; + } + + argv = (const char **) malloc (sizeof (char *) * argc); + if (argv == NULL) + { + DisplayErrorMessage ("Out of memory when allocating argv array", + ENOMEM, ErrorTitle); + goto ErrorExit; + } + + for (i = 0; i < argc; i++) + { + if ((ErrorCode = MessagePntr->FindString ("arg", i, &argv[i])) != B_OK) + { + DisplayErrorMessage ("Unable to find argument in the BMessage", + ErrorCode, ErrorTitle); + goto ErrorExit; + } + } + + CommandWord = argv[1]; + + /* Special case for the Quit command since it isn't a scripting command. */ + + if (strcasecmp (CommandWord, "quit") == 0) + { + g_QuitCountdown = 10; + goto ErrorExit; + } + + /* Find the corresponding scripting command. */ + + if (strcasecmp (CommandWord, "set") == 0) + CommandCode = B_SET_PROPERTY; + else if (strcasecmp (CommandWord, "get") == 0) + CommandCode = B_GET_PROPERTY; + else if (strcasecmp (CommandWord, "count") == 0) + CommandCode = B_COUNT_PROPERTIES; + else if (strcasecmp (CommandWord, "create") == 0) + CommandCode = B_CREATE_PROPERTY; + else if (strcasecmp (CommandWord, "delete") == 0) + CommandCode = B_DELETE_PROPERTY; + else + CommandCode = B_EXECUTE_PROPERTY; + + if (CommandCode == B_EXECUTE_PROPERTY) + { + PropertyName = CommandWord; + ArgumentIndex = 2; /* Arguments to the command start at this index. */ + } + else + { + if (CommandCode == B_SET_PROPERTY) + { + /* SET commands require at least one argument value. */ + if (argc < 4) + { + cerr << PrintUsage; + DisplayErrorMessage ("SET commands require at least one " + "argument value after the property name", -1, ErrorTitle); + goto ErrorExit; + } + } + else + if (argc < 3) + { + cerr << PrintUsage; + DisplayErrorMessage ("You need to specify a property to act on", + -1, ErrorTitle); + goto ErrorExit; + } + PropertyName = argv[2]; + ArgumentIndex = 3; + } + + /* See if it is one of our commands. */ + + for (PropInfoPntr = g_ScriptingPropertyList + 0; true; PropInfoPntr++) + { + if (PropInfoPntr->name == 0) + { + cerr << PrintUsage; + DisplayErrorMessage ("The property specified isn't known or " + "doesn't support the requested action (usually means it is an " + "unknown command)", -1, ErrorTitle); + goto ErrorExit; /* Unrecognized command. */ + } + + if (PropInfoPntr->commands[0] == CommandCode && + strcasecmp (PropertyName, PropInfoPntr->name) == 0) + break; + } + + /* Make the equivalent command message. For commands with multiple + arguments, repeat the message for each single argument and just change the + data portion for each extra argument. Send the command and wait for a reply, + which we'll print out. */ + + ScriptMessage.MakeEmpty (); + ScriptMessage.what = CommandCode; + ScriptMessage.AddSpecifier (PropertyName); + while (true) + { + if (ArgumentIndex < argc) /* If there are arguments to be added. */ + { + ValuePntr = argv[ArgumentIndex]; + + /* Convert the value into the likely kind of data. */ + + if (strcasecmp (ValuePntr, "yes") == 0 || + strcasecmp (ValuePntr, "true") == 0) + ScriptMessage.AddBool (g_DataName, true); + else if (strcasecmp (ValuePntr, "no") == 0 || + strcasecmp (ValuePntr, "false") == 0) + ScriptMessage.AddBool (g_DataName, false); + else + { + /* See if it is a number. */ + i = strtol (ValuePntr, &EndPntr, 0); + if (*EndPntr == 0) + ScriptMessage.AddInt32 (g_DataName, i); + else /* Nope, it's just a string. */ + ScriptMessage.AddString (g_DataName, ValuePntr); + } + } + + ErrorCode = be_app_messenger.SendMessage (&ScriptMessage, &ReplyMessage); + if (ErrorCode != B_OK) + { + DisplayErrorMessage ("Unable to send scripting command", + ErrorCode, ErrorTitle); + goto ErrorExit; + } + + /* Print the reply to the scripting command. Even in server mode. To + standard output. */ + + if (ReplyMessage.FindString ("CommandText", &TempStringPntr) == B_OK) + { + TempInt32 = -1; + if (ReplyMessage.FindInt32 ("error", &TempInt32) == B_OK && + TempInt32 == B_OK) + { + /* It's a successful reply to one of our scripting messages. Print out + the returned values code for command line users to see. */ + + cout << "Result of command to " << TempStringPntr << " is:\t"; + if (ReplyMessage.FindString (g_ResultName, &TempStringPntr) == B_OK) + cout << "\"" << TempStringPntr << "\""; + else if (ReplyMessage.FindInt32 (g_ResultName, &TempInt32) == B_OK) + cout << TempInt32; + else if (ReplyMessage.FindFloat (g_ResultName, &TempFloat) == B_OK) + cout << TempFloat; + else if (ReplyMessage.FindBool (g_ResultName, &TempBool) == B_OK) + cout << (TempBool ? "true" : "false"); + else + cout << "just plain success"; + if (ReplyMessage.FindInt32 ("count", &TempInt32) == B_OK) + cout << "\t(count " << TempInt32 << ")"; + for (i = 0; (i < 50) && + ReplyMessage.FindString ("words", i, &TempStringPntr) == B_OK && + ReplyMessage.FindFloat ("ratios", i, &TempFloat) == B_OK; + i++) + { + if (i == 0) + cout << "\twith top words:\t"; + else + cout << "\t"; + cout << TempStringPntr << "/" << TempFloat; + } + cout << endl; + } + else /* An error reply, print out the error, even in server mode. */ + { + cout << "Failure of command " << TempStringPntr << ", error "; + cout << TempInt32 << " (" << strerror (TempInt32) << ")"; + if (ReplyMessage.FindString ("message", &TempStringPntr) == B_OK) + cout << ", message: " << TempStringPntr; + cout << "." << endl; + } + } + + /* Advance to the next argument and its scripting message. */ + + ScriptMessage.RemoveName (g_DataName); + if (++ArgumentIndex >= argc) + break; + } + +ErrorExit: + free (argv); +} + + +/* Given a bunch of references to files, open the files. If it's a database +file, switch to using it as a database. Otherwise, treat them as text files +and add them to the database. Prompt the user for the spam or genuine or +uncertain (declassification) choice, with the option to bulk mark many files at +once. */ + +void CommanderLooper::ProcessRefs (BMessage *MessagePntr) +{ + bool BulkMode = false; + ClassificationTypes BulkClassification = CL_GENUINE; + ClassificationChoicesWindow *ChoiceWindowPntr; + BEntry Entry; + entry_ref EntryRef; + status_t ErrorCode; + const char *ErrorTitle = "CommanderLooper::ProcessRefs"; + int32 NumberOfRefs = 0; + BPath Path; + int RefIndex; + BMessage ReplyMessage; + BMessage ScriptingMessage; + bool TempBool; + BFile TempFile; + int32 TempInt32; + char TempString [PATH_MAX + 1024]; + type_code TypeCode; + + // Wait for ReadyToRun to finish initializing the globals with the sizes of + // the controls, since they are needed when we show the custom alert box for + // choosing the message type. + + TempInt32 = 0; + while (!g_AppReadyToRunCompleted && TempInt32++ < 10) + snooze (200000); + + ErrorCode = MessagePntr->GetInfo ("refs", &TypeCode, &NumberOfRefs); + if (ErrorCode != B_OK || TypeCode != B_REF_TYPE || NumberOfRefs <= 0) + { + DisplayErrorMessage ("Unable to get refs from the message", + ErrorCode, ErrorTitle); + return; + } + + if (MessagePntr->FindBool ("BulkMode", &TempBool) == B_OK) + BulkMode = TempBool; + if (MessagePntr->FindInt32 ("BulkClassification", &TempInt32) == B_OK && + TempInt32 >= 0 && TempInt32 < CL_MAX) + BulkClassification = (ClassificationTypes) TempInt32; + + for (RefIndex = 0; + MessagePntr->FindRef ("refs", RefIndex, &EntryRef) == B_OK; + RefIndex++) + { + ScriptingMessage.MakeEmpty (); + ScriptingMessage.what = 0; /* Haven't figured out what to do yet. */ + + /* See if the entry is a valid file or directory or other thing. */ + + ErrorCode = Entry.SetTo (&EntryRef, true /* traverse symbolic links */); + if (ErrorCode != B_OK || + ((ErrorCode = /* assignment */ B_ENTRY_NOT_FOUND) != 0 /* this pacifies + mwcc -nwhitehorn */ && !Entry.Exists ()) || + ((ErrorCode = Entry.GetPath (&Path)) != B_OK)) + { + DisplayErrorMessage ("Bad entry reference encountered, will skip it", + ErrorCode, ErrorTitle); + BulkMode = false; + continue; /* Bad file reference, try the next one. */ + } + + /* If it's a file, check if it is a spam database file. Go by the magic + text at the start of the file, in case someone has edited the file with a + spreadsheet or other tool and lost the MIME type. */ + + if (Entry.IsFile ()) + { + ErrorCode = TempFile.SetTo (&Entry, B_READ_ONLY); + if (ErrorCode != B_OK) + { + sprintf (TempString, "Unable to open file \"%s\" for reading, will " + "skip it", Path.Path ()); + DisplayErrorMessage (TempString, ErrorCode, ErrorTitle); + BulkMode = false; + continue; + } + if (TempFile.Read (TempString, strlen (g_DatabaseRecognitionString)) == + (int) strlen (g_DatabaseRecognitionString) && strncmp (TempString, + g_DatabaseRecognitionString, strlen (g_DatabaseRecognitionString)) == 0) + { + ScriptingMessage.what = B_SET_PROPERTY; + ScriptingMessage.AddSpecifier (g_PropertyNames[PN_DATABASE_FILE]); + ScriptingMessage.AddString (g_DataName, Path.Path ()); + } + TempFile.Unset (); + } + + /* Not a database file. Could be a directory or a file. Submit it as + something to be marked spam or genuine. */ + + if (ScriptingMessage.what == 0) + { + if (!Entry.IsFile ()) + { + sprintf (TempString, "\"%s\" is not a file, can't do anything with it", + Path.Path ()); + DisplayErrorMessage (TempString, -1, ErrorTitle); + BulkMode = false; + continue; + } + + if (!BulkMode) /* Have to ask the user. */ + { + ChoiceWindowPntr = new ClassificationChoicesWindow ( + BRect (40, 40, 40 + 50 * g_MarginBetweenControls, + 40 + g_ButtonHeight * 5), Path.Path (), NumberOfRefs - RefIndex); + ChoiceWindowPntr->Go (&BulkMode, &BulkClassification); + if (BulkClassification == CL_MAX) + break; /* Cancel was picked. */ + } + + /* Format the command for classifying the file. */ + + ScriptingMessage.what = B_SET_PROPERTY; + + if (BulkClassification == CL_GENUINE) + ScriptingMessage.AddSpecifier (g_PropertyNames[PN_GENUINE]); + else if (BulkClassification == CL_SPAM) + ScriptingMessage.AddSpecifier (g_PropertyNames[PN_SPAM]); + else if (BulkClassification == CL_UNCERTAIN) + ScriptingMessage.AddSpecifier (g_PropertyNames[PN_UNCERTAIN]); + else /* Broken code */ + break; + ScriptingMessage.AddString (g_DataName, Path.Path ()); + } + + /* Tell the BApplication to do the work, and wait for it to finish. The + BApplication will display any error messages for us. */ + + ErrorCode = + be_app_messenger.SendMessage (&ScriptingMessage, &ReplyMessage); + if (ErrorCode != B_OK) + { + DisplayErrorMessage ("Unable to send scripting command", + ErrorCode, ErrorTitle); + return; + } + + /* If there was an error, allow the user to stop by switching off bulk + mode. The message will already have been displayed in an alert box, if + server mode is off. */ + + if (ReplyMessage.FindInt32 ("error", &TempInt32) != B_OK || + TempInt32 != B_OK) + BulkMode = false; + } +} + + + +/****************************************************************************** + * Implementation of the ControlsView class, constructor, destructor and the + * rest of the member functions in mostly alphabetical order. + */ + +ControlsView::ControlsView (BRect NewBounds) +: BView (NewBounds, "ControlsView", B_FOLLOW_TOP | B_FOLLOW_LEFT_RIGHT, + B_WILL_DRAW | B_PULSE_NEEDED | B_NAVIGABLE_JUMP | B_FRAME_EVENTS), + m_AboutButtonPntr (NULL), + m_AddExampleButtonPntr (NULL), + m_BrowseButtonPntr (NULL), + m_BrowseFilePanelPntr (NULL), + m_CreateDatabaseButtonPntr (NULL), + m_DatabaseFileNameTextboxPntr (NULL), + m_DatabaseLoadDone (false), + m_EstimateSpamButtonPntr (NULL), + m_EstimateSpamFilePanelPntr (NULL), + m_GenuineCountTextboxPntr (NULL), + m_IgnorePreviousClassCheckboxPntr (NULL), + m_InstallThingsButtonPntr (NULL), + m_PurgeAgeTextboxPntr (NULL), + m_PurgeButtonPntr (NULL), + m_PurgePopularityTextboxPntr (NULL), + m_ResetToDefaultsButtonPntr (NULL), + m_ScoringModeMenuBarPntr (NULL), + m_ScoringModePopUpMenuPntr (NULL), + m_ServerModeCheckboxPntr (NULL), + m_SpamCountTextboxPntr (NULL), + m_TimeOfLastPoll (0), + m_TokenizeModeMenuBarPntr (NULL), + m_TokenizeModePopUpMenuPntr (NULL), + m_WordCountTextboxPntr (NULL) +{ +} + + +ControlsView::~ControlsView () +{ + if (m_BrowseFilePanelPntr != NULL) + { + delete m_BrowseFilePanelPntr; + m_BrowseFilePanelPntr = NULL; + } + + if (m_EstimateSpamFilePanelPntr != NULL) + { + delete m_EstimateSpamFilePanelPntr; + m_EstimateSpamFilePanelPntr = NULL; + } +} + + +void ControlsView::AttachedToWindow () +{ + float BigPurgeButtonTop; + BMessage CommandMessage; + const char *EightDigitsString = " 12345678 "; + float Height; + float Margin; + float RowHeight; + float RowTop; + ScoringModes ScoringMode; + char *StringPntr; + BMenuItem *TempMenuItemPntr; + BRect TempRect; + char TempString [PATH_MAX]; + TokenizeModes TokenizeMode; + float Width; + float X; + + SetViewColor (ui_color (B_PANEL_BACKGROUND_COLOR)); + + TempRect = Bounds (); + X = TempRect.right; + RowTop = TempRect.top; + RowHeight = g_ButtonHeight; + if (g_TextBoxHeight > RowHeight) + RowHeight = g_TextBoxHeight; + RowHeight = ceilf (RowHeight * 1.1); + + /* Make the Create button at the far right of the first row of controls, + which are all database file related. */ + + Margin = ceilf ((RowHeight - g_ButtonHeight) / 2); + TempRect = Bounds (); + TempRect.top = RowTop + Margin; + TempRect.bottom = TempRect.top + g_ButtonHeight; + + CommandMessage.MakeEmpty (); + CommandMessage.what = B_CREATE_PROPERTY; + CommandMessage.AddSpecifier (g_PropertyNames[PN_DATABASE_FILE]); + m_CreateDatabaseButtonPntr = new BButton (TempRect, "Create Button", + "Create", new BMessage (CommandMessage), B_FOLLOW_RIGHT | B_FOLLOW_TOP); + if (m_CreateDatabaseButtonPntr == NULL) goto ErrorExit; + AddChild (m_CreateDatabaseButtonPntr); + m_CreateDatabaseButtonPntr->SetTarget (be_app); + m_CreateDatabaseButtonPntr->ResizeToPreferred (); + m_CreateDatabaseButtonPntr->GetPreferredSize (&Width, &Height); + m_CreateDatabaseButtonPntr->MoveTo (X - Width, TempRect.top); + X -= Width + g_MarginBetweenControls; + + /* Make the Browse button, middle of the first row. */ + + Margin = ceilf ((RowHeight - g_ButtonHeight) / 2); + TempRect = Bounds (); + TempRect.top = RowTop + Margin; + TempRect.bottom = TempRect.top + g_ButtonHeight; + + m_BrowseButtonPntr = new BButton (TempRect, "Browse Button", + "Browse", new BMessage (MSG_BROWSE_BUTTON), B_FOLLOW_RIGHT | B_FOLLOW_TOP); + if (m_BrowseButtonPntr == NULL) goto ErrorExit; + AddChild (m_BrowseButtonPntr); + m_BrowseButtonPntr->SetTarget (this); + m_BrowseButtonPntr->ResizeToPreferred (); + m_BrowseButtonPntr->GetPreferredSize (&Width, &Height); + m_BrowseButtonPntr->MoveTo (X - Width, TempRect.top); + X -= Width + g_MarginBetweenControls; + + /* Fill the rest of the space on the first row with the file name box. */ + + Margin = ceilf ((RowHeight - g_TextBoxHeight) / 2); + TempRect = Bounds (); + TempRect.top = RowTop + Margin; + TempRect.bottom = TempRect.top + g_TextBoxHeight; + TempRect.right = X; + + StringPntr = "Database File Name:"; + strcpy (m_DatabaseFileNameCachedValue, "Unknown..."); + m_DatabaseFileNameTextboxPntr = new BTextControl (TempRect, + "File Name", + StringPntr /* label */, + m_DatabaseFileNameCachedValue /* text */, + new BMessage (MSG_DATABASE_NAME), + B_FOLLOW_LEFT_RIGHT | B_FOLLOW_TOP, + B_WILL_DRAW | B_NAVIGABLE | B_NAVIGABLE_JUMP); + AddChild (m_DatabaseFileNameTextboxPntr); + m_DatabaseFileNameTextboxPntr->SetTarget (this); + m_DatabaseFileNameTextboxPntr->SetDivider ( + be_plain_font->StringWidth (StringPntr) + g_MarginBetweenControls); + + /* Second row contains the purge age, and a long line explaining it. There + is space to the right where the top half of the big purge button will go. */ + + RowTop += RowHeight /* previous row's RowHeight */; + BigPurgeButtonTop = RowTop; + TempRect = Bounds (); + X = TempRect.left; + RowHeight = g_TextBoxHeight; + RowHeight = ceilf (RowHeight * 1.1); + + StringPntr = "Purge Age (words added this many or more messages " + "ago may get deleted):"; + m_PurgeAgeCachedValue = 12345678; + + Margin = ceilf ((RowHeight - g_TextBoxHeight) / 2); + TempRect.top = RowTop + Margin; + TempRect.bottom = TempRect.top + g_TextBoxHeight; + TempRect.left = X; + TempRect.right = TempRect.left + + be_plain_font->StringWidth (StringPntr) + + be_plain_font->StringWidth (EightDigitsString) + + 3 * g_MarginBetweenControls; + + sprintf (TempString, "%d", (int) m_PurgeAgeCachedValue); + m_PurgeAgeTextboxPntr = new BTextControl (TempRect, + "Purge Age", + StringPntr /* label */, + TempString /* text */, + new BMessage (MSG_PURGE_AGE), + B_FOLLOW_LEFT | B_FOLLOW_TOP, + B_WILL_DRAW | B_NAVIGABLE); + AddChild (m_PurgeAgeTextboxPntr); + m_PurgeAgeTextboxPntr->SetTarget (this); + m_PurgeAgeTextboxPntr->SetDivider ( + be_plain_font->StringWidth (StringPntr) + g_MarginBetweenControls); + + /* Third row contains the purge popularity and bottom half of the purge + button. */ + + RowTop += RowHeight /* previous row's RowHeight */; + TempRect = Bounds (); + X = TempRect.left; + RowHeight = g_TextBoxHeight; + RowHeight = ceilf (RowHeight * 1.1); + + StringPntr = "Purge Popularity (words that occur in this many or fewer " + "e-mail messages may get deleted):"; + m_PurgePopularityCachedValue = 87654321; + Margin = ceilf ((RowHeight - g_TextBoxHeight) / 2); + TempRect.top = RowTop + Margin; + TempRect.bottom = TempRect.top + g_TextBoxHeight; + TempRect.left = X; + TempRect.right = TempRect.left + + be_plain_font->StringWidth (StringPntr) + + be_plain_font->StringWidth (EightDigitsString) + + 3 * g_MarginBetweenControls; + X = TempRect.right + g_MarginBetweenControls; + + sprintf (TempString, "%d", (int) m_PurgePopularityCachedValue); + m_PurgePopularityTextboxPntr = new BTextControl (TempRect, + "Purge Popularity", + StringPntr /* label */, + TempString /* text */, + new BMessage (MSG_PURGE_POPULARITY), + B_FOLLOW_LEFT | B_FOLLOW_TOP, + B_WILL_DRAW | B_NAVIGABLE); + AddChild (m_PurgePopularityTextboxPntr); + m_PurgePopularityTextboxPntr->SetTarget (this); + m_PurgePopularityTextboxPntr->SetDivider ( + be_plain_font->StringWidth (StringPntr) + g_MarginBetweenControls); + + /* Make the purge button, which will take up space in the 2nd and 3rd rows, + on the right side. Twice as tall as a regular button too. */ + + StringPntr = "Purge!"; + Margin = ceilf ((((RowTop + RowHeight) - BigPurgeButtonTop) - + 2 * g_TextBoxHeight) / 2); + TempRect.top = BigPurgeButtonTop + Margin; + TempRect.bottom = TempRect.top + 2 * g_TextBoxHeight; + TempRect.left = X; + TempRect.right = X + ceilf (2 * be_plain_font->StringWidth (StringPntr)); + + CommandMessage.MakeEmpty (); + CommandMessage.what = B_EXECUTE_PROPERTY; + CommandMessage.AddSpecifier (g_PropertyNames[PN_PURGE]); + m_PurgeButtonPntr = new BButton (TempRect, "Purge Button", + StringPntr, new BMessage (CommandMessage), B_FOLLOW_LEFT | B_FOLLOW_TOP); + if (m_PurgeButtonPntr == NULL) goto ErrorExit; + AddChild (m_PurgeButtonPntr); + m_PurgeButtonPntr->SetTarget (be_app); + + /* The fourth row contains the ignore previous classification checkbox. */ + + RowTop += RowHeight /* previous row's RowHeight */; + TempRect = Bounds (); + X = TempRect.left; + RowHeight = g_CheckBoxHeight; + RowHeight = ceilf (RowHeight * 1.1); + + StringPntr = "Ignore Previous Classification (so that previously classified " + "messages can be added again, otherwise only a change in classification " + "is recognized)"; + m_IgnorePreviousClassCachedValue = false; + + Margin = ceilf ((RowHeight - g_CheckBoxHeight) / 2); + TempRect.top = RowTop + Margin; + TempRect.bottom = TempRect.top + g_CheckBoxHeight; + TempRect.left = X; + m_IgnorePreviousClassCheckboxPntr = new BCheckBox (TempRect, + "Ignore Check", + StringPntr, + new BMessage (MSG_IGNORE_CLASSIFICATION), + B_FOLLOW_TOP | B_FOLLOW_LEFT); + if (m_IgnorePreviousClassCheckboxPntr == NULL) goto ErrorExit; + AddChild (m_IgnorePreviousClassCheckboxPntr); + m_IgnorePreviousClassCheckboxPntr->SetTarget (this); + m_IgnorePreviousClassCheckboxPntr->ResizeToPreferred (); + m_IgnorePreviousClassCheckboxPntr->GetPreferredSize (&Width, &Height); + X += Width + g_MarginBetweenControls; + + /* The fifth row contains the server mode checkbox. */ + + RowTop += RowHeight /* previous row's RowHeight */; + TempRect = Bounds (); + RowHeight = g_CheckBoxHeight; + RowHeight = ceilf (RowHeight * 1.1); + + StringPntr = "Server Mode (errors printed on the standard error stream " + "rather than in alert boxes, window minimized)"; + m_ServerModeCachedValue = false; + + Margin = ceilf ((RowHeight - g_CheckBoxHeight) / 2); + TempRect.top = RowTop + Margin; + TempRect.bottom = TempRect.top + g_CheckBoxHeight; + m_ServerModeCheckboxPntr = new BCheckBox (TempRect, + "ServerMode Check", + StringPntr, + new BMessage (MSG_SERVER_MODE), + B_FOLLOW_TOP | B_FOLLOW_LEFT); + if (m_ServerModeCheckboxPntr == NULL) goto ErrorExit; + AddChild (m_ServerModeCheckboxPntr); + m_ServerModeCheckboxPntr->SetTarget (this); + m_ServerModeCheckboxPntr->ResizeToPreferred (); + m_ServerModeCheckboxPntr->GetPreferredSize (&Width, &Height); + + /* This row just contains a huge pop-up menu which shows the tokenize mode + and an explanation of what each mode does. */ + + RowTop += RowHeight /* previous row's RowHeight */; + TempRect = Bounds (); + RowHeight = g_PopUpMenuHeight; + RowHeight = ceilf (RowHeight * 1.1); + + Margin = ceilf ((RowHeight - g_PopUpMenuHeight) / 2); + TempRect.top = RowTop + Margin; + TempRect.bottom = TempRect.top + g_PopUpMenuHeight; + + m_TokenizeModeCachedValue = TM_MAX; /* Illegal value will force redraw. */ + m_TokenizeModeMenuBarPntr = new BMenuBar (TempRect, "TokenizeModeMenuBar", + B_FOLLOW_LEFT_RIGHT | B_FOLLOW_TOP, B_ITEMS_IN_COLUMN, + false /* resize to fit items */); + if (m_TokenizeModeMenuBarPntr == NULL) goto ErrorExit; + m_TokenizeModePopUpMenuPntr = new BPopUpMenu ("TokenizeModePopUpMenu"); + if (m_TokenizeModePopUpMenuPntr == NULL) goto ErrorExit; + + for (TokenizeMode = (TokenizeModes) 0; + TokenizeMode < TM_MAX; + TokenizeMode = (TokenizeModes) ((int) TokenizeMode + 1)) + { + /* Each different tokenize mode gets its own menu item. Selecting the item + will send a canned command to the application to switch to the appropriate + tokenize mode. An optional explanation of each mode is added to the mode + name string. */ + + CommandMessage.MakeEmpty (); + CommandMessage.what = B_SET_PROPERTY; + CommandMessage.AddSpecifier (g_PropertyNames[PN_TOKENIZE_MODE]); + CommandMessage.AddString (g_DataName, g_TokenizeModeNames[TokenizeMode]); + strcpy (TempString, g_TokenizeModeNames[TokenizeMode]); + switch (TokenizeMode) + { + case TM_WHOLE: + strcat (TempString, " - Looks for words in the whole file, so it " + "sees everything! Use this for non-Email files too. Also does " + "a rough quoted-printable decoding."); + break; + + case TM_PLAIN_TEXT: + strcat (TempString, " - Only examines text/plain parts of the " + "e-mail (no HTML parts). But does decode base64 etc. and do " + "character set conversions (all but Whole also do that)."); + break; + + case TM_PLAIN_TEXT_HEADER: + strcat (TempString, " - Examines text/plain parts of the " + "e-mail (no HTML parts), and also looks at the headers (from, to, " + "subject, names of attached files, etc)."); + break; + + case TM_ANY_TEXT: + strcat (TempString, " - Examines all text/* parts of the " + "e-mail, including HTML parts."); + break; + + case TM_ANY_TEXT_HEADER: + strcat (TempString, " - Examines all text/* parts of the " + "e-mail, including HTML parts. Also examines the headers."); + break; + + case TM_ALL_PARTS: + strcat (TempString, " - Examines all parts of the e-mail, including " + "HTML parts and binary attachments (mostly useless garbage words)."); + break; + + case TM_ALL_PARTS_HEADER: + strcat (TempString, " - Examines all parts of the e-mail, including " + "HTML parts and binary attachments, plus the headers."); + break; + + case TM_JUST_HEADER: + strcat (TempString, " - Only examines the first header part of the " + "e-mail (from, to, subject, but not MIME subheaders (names of " + "attached files and other attachment info))."); + break; + + default: + break; + } + TempMenuItemPntr = + new BMenuItem (TempString, new BMessage (CommandMessage)); + if (TempMenuItemPntr == NULL) goto ErrorExit; + TempMenuItemPntr->SetTarget (be_app); + m_TokenizeModePopUpMenuPntr->AddItem (TempMenuItemPntr); + } + m_TokenizeModeMenuBarPntr->AddItem (m_TokenizeModePopUpMenuPntr); + AddChild (m_TokenizeModeMenuBarPntr); + + /* This row just contains a huge pop-up menu which shows the scoring mode + and an explanation of what each mode does. */ + + RowTop += RowHeight /* previous row's RowHeight */; + TempRect = Bounds (); + RowHeight = g_PopUpMenuHeight; + RowHeight = ceilf (RowHeight * 1.1); + + Margin = ceilf ((RowHeight - g_PopUpMenuHeight) / 2); + TempRect.top = RowTop + Margin; + TempRect.bottom = TempRect.top + g_PopUpMenuHeight; + + m_ScoringModeCachedValue = SM_MAX; /* Illegal value will force redraw. */ + m_ScoringModeMenuBarPntr = new BMenuBar (TempRect, "ScoringModeMenuBar", + B_FOLLOW_LEFT_RIGHT | B_FOLLOW_TOP, B_ITEMS_IN_COLUMN, + false /* resize to fit items */); + if (m_ScoringModeMenuBarPntr == NULL) goto ErrorExit; + m_ScoringModePopUpMenuPntr = new BPopUpMenu ("ScoringModePopUpMenu"); + if (m_ScoringModePopUpMenuPntr == NULL) goto ErrorExit; + + for (ScoringMode = (ScoringModes) 0; + ScoringMode < SM_MAX; + ScoringMode = (ScoringModes) ((int) ScoringMode + 1)) + { + /* Each different scoring mode gets its own menu item. Selecting the item + will send a canned command to the application to switch to the appropriate + scoring mode. An optional explanation of each mode is added to the mode + name string. */ + + CommandMessage.MakeEmpty (); + CommandMessage.what = B_SET_PROPERTY; + CommandMessage.AddSpecifier (g_PropertyNames[PN_SCORING_MODE]); + CommandMessage.AddString (g_DataName, g_ScoringModeNames[ScoringMode]); + strcpy (TempString, g_ScoringModeNames[ScoringMode]); + switch (ScoringMode) + { + case SM_ROBINSON: + strcat (TempString, " - Gary Robinson's invention. Score varies " + "evenly between 0 and 1, so you get shades of spaminess. Cutoff " + "point varies depending on your training examples."); + break; + + case SM_CHISQUARED: + strcat (TempString, " - Uses Chi-Squared statistics test. Score " + "near 0 for genuine, near 1 for spam and inbetween if it is " + "unsure. No shades of spaminess."); + break; + + default: + break; + } + TempMenuItemPntr = + new BMenuItem (TempString, new BMessage (CommandMessage)); + if (TempMenuItemPntr == NULL) goto ErrorExit; + TempMenuItemPntr->SetTarget (be_app); + m_ScoringModePopUpMenuPntr->AddItem (TempMenuItemPntr); + } + m_ScoringModeMenuBarPntr->AddItem (m_ScoringModePopUpMenuPntr); + AddChild (m_ScoringModeMenuBarPntr); + + /* The next row has the install MIME types button and the reset to defaults + button, one on the left and the other on the right. */ + + RowTop += RowHeight /* previous row's RowHeight */; + TempRect = Bounds (); + RowHeight = g_ButtonHeight; + RowHeight = ceilf (RowHeight * 1.1); + + Margin = ceilf ((RowHeight - g_ButtonHeight) / 2); + TempRect.top = RowTop + Margin; + TempRect.bottom = TempRect.top + g_ButtonHeight; + + CommandMessage.MakeEmpty (); + CommandMessage.what = B_EXECUTE_PROPERTY; + CommandMessage.AddSpecifier (g_PropertyNames[PN_INSTALL_THINGS]); + m_InstallThingsButtonPntr = new BButton (TempRect, "Install Button", + "Install MIME Types & Make Indices on All Drives", + new BMessage (CommandMessage), + B_FOLLOW_LEFT | B_FOLLOW_TOP); + if (m_InstallThingsButtonPntr == NULL) goto ErrorExit; + AddChild (m_InstallThingsButtonPntr); + m_InstallThingsButtonPntr->SetTarget (be_app); + m_InstallThingsButtonPntr->ResizeToPreferred (); + + /* The Reset to Defaults button. On the right side of the row. */ + + Margin = ceilf ((RowHeight - g_ButtonHeight) / 2); + TempRect = Bounds (); + TempRect.top = RowTop + Margin; + TempRect.bottom = TempRect.top + g_ButtonHeight; + + CommandMessage.MakeEmpty (); + CommandMessage.what = B_EXECUTE_PROPERTY; + CommandMessage.AddSpecifier (g_PropertyNames[PN_RESET_TO_DEFAULTS]); + m_ResetToDefaultsButtonPntr = new BButton (TempRect, "Reset Button", + "Reset Settings to Defaults", new BMessage (CommandMessage), + B_FOLLOW_RIGHT | B_FOLLOW_TOP); + if (m_ResetToDefaultsButtonPntr == NULL) goto ErrorExit; + AddChild (m_ResetToDefaultsButtonPntr); + m_ResetToDefaultsButtonPntr->SetTarget (be_app); + m_ResetToDefaultsButtonPntr->ResizeToPreferred (); + m_ResetToDefaultsButtonPntr->GetPreferredSize (&Width, &Height); + m_ResetToDefaultsButtonPntr->MoveTo (TempRect.right - Width, TempRect.top); + + /* The next row contains the Estimate, Add Examples and About buttons. */ + + RowTop += RowHeight /* previous row's RowHeight */; + TempRect = Bounds (); + X = TempRect.left; + RowHeight = g_ButtonHeight; + RowHeight = ceilf (RowHeight * 1.1); + + Margin = ceilf ((RowHeight - g_ButtonHeight) / 2); + TempRect.top = RowTop + Margin; + TempRect.bottom = TempRect.top + g_ButtonHeight; + TempRect.left = X; + + m_EstimateSpamButtonPntr = new BButton (TempRect, "Estimate Button", + "Estimate Spam Ratio of a New Message", + new BMessage (MSG_ESTIMATE_BUTTON), + B_FOLLOW_LEFT | B_FOLLOW_TOP); + if (m_EstimateSpamButtonPntr == NULL) goto ErrorExit; + AddChild (m_EstimateSpamButtonPntr); + m_EstimateSpamButtonPntr->SetTarget (this); + m_EstimateSpamButtonPntr->ResizeToPreferred (); + X = m_EstimateSpamButtonPntr->Frame().right + g_MarginBetweenControls; + + /* The Add Example button in the middle. Does the same as the browse button, + but don't tell anyone that! */ + + Margin = ceilf ((RowHeight - g_ButtonHeight) / 2); + TempRect.top = RowTop + Margin; + TempRect.bottom = TempRect.top + g_ButtonHeight; + TempRect.left = X; + + m_AddExampleButtonPntr = new BButton (TempRect, "Example Button", + "Add Example of Spam/Genuine", + new BMessage (MSG_BROWSE_BUTTON), + B_FOLLOW_LEFT_RIGHT | B_FOLLOW_TOP, + B_WILL_DRAW | B_NAVIGABLE | B_FULL_UPDATE_ON_RESIZE); + if (m_AddExampleButtonPntr == NULL) goto ErrorExit; + AddChild (m_AddExampleButtonPntr); + m_AddExampleButtonPntr->SetTarget (this); + m_AddExampleButtonPntr->ResizeToPreferred (); + X = m_AddExampleButtonPntr->Frame().right + g_MarginBetweenControls; + + /* Add the About button on the right. */ + + Margin = ceilf ((RowHeight - g_ButtonHeight) / 2); + TempRect = Bounds (); + TempRect.top = RowTop + Margin; + TempRect.bottom = TempRect.top + g_ButtonHeight; + TempRect.left = X; + + m_AboutButtonPntr = new BButton (TempRect, "About Button", + "About this fine program...", + new BMessage (B_ABOUT_REQUESTED), + B_FOLLOW_RIGHT | B_FOLLOW_TOP); + if (m_AboutButtonPntr == NULL) goto ErrorExit; + AddChild (m_AboutButtonPntr); + m_AboutButtonPntr->SetTarget (be_app); + + /* This row displays various counters. Starting with the genuine messages + count on the left. */ + + RowTop += RowHeight /* previous row's RowHeight */; + TempRect = Bounds (); + RowHeight = g_TextBoxHeight; + RowHeight = ceilf (RowHeight * 1.1); + + StringPntr = "Genuine Messages:"; + m_GenuineCountCachedValue = 87654321; + sprintf (TempString, "%d", (int) m_GenuineCountCachedValue); + + Margin = ceilf ((RowHeight - g_TextBoxHeight) / 2); + TempRect = Bounds (); + TempRect.top = RowTop + Margin; + TempRect.bottom = TempRect.top + g_TextBoxHeight; + TempRect.right = TempRect.left + + be_plain_font->StringWidth (StringPntr) + + be_plain_font->StringWidth (TempString) + + 3 * g_MarginBetweenControls; + + m_GenuineCountTextboxPntr = new BTextControl (TempRect, + "Genuine Count", + StringPntr /* label */, + TempString /* text */, + NULL /* no message */, + B_FOLLOW_LEFT | B_FOLLOW_TOP, + B_WILL_DRAW /* not B_NAVIGABLE */); + AddChild (m_GenuineCountTextboxPntr); + m_GenuineCountTextboxPntr->SetTarget (this); /* Not that it matters. */ + m_GenuineCountTextboxPntr->SetDivider ( + be_plain_font->StringWidth (StringPntr) + g_MarginBetweenControls); + m_GenuineCountTextboxPntr->SetEnabled (false); /* For display only. */ + + /* The word count in the center. */ + + StringPntr = "Word Count:"; + m_WordCountCachedValue = 87654321; + sprintf (TempString, "%d", (int) m_WordCountCachedValue); + + Margin = ceilf ((RowHeight - g_TextBoxHeight) / 2); + TempRect = Bounds (); + TempRect.top = RowTop + Margin; + TempRect.bottom = TempRect.top + g_TextBoxHeight; + Width = be_plain_font->StringWidth (StringPntr) + + be_plain_font->StringWidth (TempString) + + 3 * g_MarginBetweenControls; + TempRect.left = ceilf ((TempRect.right - TempRect.left) / 2 - Width / 2); + TempRect.right = TempRect.left + Width; + + m_WordCountTextboxPntr = new BTextControl (TempRect, + "Word Count", + StringPntr /* label */, + TempString /* text */, + NULL /* no message */, + B_FOLLOW_H_CENTER | B_FOLLOW_TOP, + B_WILL_DRAW /* not B_NAVIGABLE */); + AddChild (m_WordCountTextboxPntr); + m_WordCountTextboxPntr->SetTarget (this); /* Not that it matters. */ + m_WordCountTextboxPntr->SetDivider ( + be_plain_font->StringWidth (StringPntr) + g_MarginBetweenControls); + m_WordCountTextboxPntr->SetEnabled (false); /* For display only. */ + + /* The spam count on the far right. */ + + StringPntr = "Spam Messages:"; + m_SpamCountCachedValue = 87654321; + sprintf (TempString, "%d", (int) m_SpamCountCachedValue); + + Margin = ceilf ((RowHeight - g_TextBoxHeight) / 2); + TempRect = Bounds (); + TempRect.top = RowTop + Margin; + TempRect.bottom = TempRect.top + g_TextBoxHeight; + TempRect.left = TempRect.right - + be_plain_font->StringWidth (StringPntr) - + be_plain_font->StringWidth (TempString) - + 3 * g_MarginBetweenControls; + + m_SpamCountTextboxPntr = new BTextControl (TempRect, + "Spam Count", + StringPntr /* label */, + TempString /* text */, + NULL /* no message */, + B_FOLLOW_RIGHT | B_FOLLOW_TOP, + B_WILL_DRAW /* not B_NAVIGABLE */); + AddChild (m_SpamCountTextboxPntr); + m_SpamCountTextboxPntr->SetTarget (this); /* Not that it matters. */ + m_SpamCountTextboxPntr->SetDivider ( + be_plain_font->StringWidth (StringPntr) + g_MarginBetweenControls); + m_SpamCountTextboxPntr->SetEnabled (false); /* For display only. */ + + /* Change the size of our view so it only takes up the space needed by the + buttons. */ + + RowTop += RowHeight /* previous row's RowHeight */; + ResizeTo (Bounds().Width(), RowTop - Bounds().top + 1); + + return; /* Successful. */ + +ErrorExit: + DisplayErrorMessage ("Unable to initialise the controls view."); +} + + +void ControlsView::BrowseForDatabaseFile () +{ + if (m_BrowseFilePanelPntr == NULL) + { + BEntry DirectoryEntry; + entry_ref DirectoryEntryRef; + BMessage GetDatabasePathCommand; + BMessage GetDatabasePathResult; + const char *StringPntr = NULL; + + /* Create a new file panel. First set up the entry ref stuff so that the + file panel can open to show the initial directory (the one where the + database file currently is). Note that we have to create it after the + window and view are up and running, otherwise the BMessenger won't point to + a valid looper/handler. First find out the current database file name to + use as a starting point. */ + + GetDatabasePathCommand.what = B_GET_PROPERTY; + GetDatabasePathCommand.AddSpecifier (g_PropertyNames[PN_DATABASE_FILE]); + be_app_messenger.SendMessage (&GetDatabasePathCommand, + &GetDatabasePathResult, 5000000 /* delivery timeout */, + 5000000 /* reply timeout */); + if (GetDatabasePathResult.FindString (g_ResultName, &StringPntr) != B_OK || + DirectoryEntry.SetTo (StringPntr) != B_OK || + DirectoryEntry.GetParent (&DirectoryEntry) != B_OK) + DirectoryEntry.SetTo ("."); /* Default directory if we can't find it. */ + if (DirectoryEntry.GetRef (&DirectoryEntryRef) != B_OK) + { + DisplayErrorMessage ( + "Unable to set up the file requestor starting directory. Sorry."); + return; + } + + m_BrowseFilePanelPntr = new BFilePanel ( + B_OPEN_PANEL /* mode */, + &be_app_messenger /* target for event messages */, + &DirectoryEntryRef /* starting directory */, + B_FILE_NODE, + true /* true for multiple selections */, + NULL /* canned message */, + NULL /* ref filter */, + false /* true for modal */, + true /* true to hide when done */); + } + + if (m_BrowseFilePanelPntr != NULL) + m_BrowseFilePanelPntr->Show (); /* Answer returned later in RefsReceived. */ +} + + +void ControlsView::BrowseForFileToEstimate () +{ + if (m_EstimateSpamFilePanelPntr == NULL) + { + BEntry DirectoryEntry; + entry_ref DirectoryEntryRef; + status_t ErrorCode; + BMessenger MessengerToSelf (this); + BPath PathToMailDirectory; + + /* Create a new file panel. First set up the entry ref stuff so that the + file panel can open to show the initial directory (the user's mail + directory). Note that we have to create the panel after the window and + view are up and running, otherwise the BMessenger won't point to a valid + looper/handler. */ + + ErrorCode = find_directory (B_USER_DIRECTORY, &PathToMailDirectory); + if (ErrorCode == B_OK) + { + PathToMailDirectory.Append ("mail"); + ErrorCode = DirectoryEntry.SetTo (PathToMailDirectory.Path(), + true /* traverse symbolic links*/); + if (ErrorCode != B_OK || !DirectoryEntry.Exists ()) + { + /* If no mail directory, try home directory. */ + find_directory (B_USER_DIRECTORY, &PathToMailDirectory); + ErrorCode = DirectoryEntry.SetTo (PathToMailDirectory.Path(), true); + } + } + if (ErrorCode != B_OK) + PathToMailDirectory.SetTo ("."); + + DirectoryEntry.SetTo (PathToMailDirectory.Path(), true); + if (DirectoryEntry.GetRef (&DirectoryEntryRef) != B_OK) + { + DisplayErrorMessage ( + "Unable to set up the file requestor starting directory. Sorry."); + return; + } + + m_EstimateSpamFilePanelPntr = new BFilePanel ( + B_OPEN_PANEL /* mode */, + &MessengerToSelf /* target for event messages */, + &DirectoryEntryRef /* starting directory */, + B_FILE_NODE, + true /* true for multiple selections */, + new BMessage (MSG_ESTIMATE_FILE_REFS) /* canned message */, + NULL /* ref filter */, + false /* true for modal */, + true /* true to hide when done */); + } + + if (m_EstimateSpamFilePanelPntr != NULL) + m_EstimateSpamFilePanelPntr->Show (); /* Answer sent via a message. */ +} + + +/* The display has been resized. Have to manually adjust the popup menu bar to +show the new size (the sub-items need to be resized too). Then make it redraw. +Well, actually just resetting the mark on the current item will resize it +properly. */ + +void ControlsView::FrameResized (float, float) +{ + m_ScoringModeCachedValue = SM_MAX; /* Force it to reset the mark. */ + m_TokenizeModeCachedValue = TM_MAX; /* Force it to reset the mark. */ +} + + +void ControlsView::MessageReceived (BMessage *MessagePntr) +{ + BMessage CommandMessage; + bool TempBool; + uint32 TempUint32; + + switch (MessagePntr->what) + { + case MSG_BROWSE_BUTTON: + BrowseForDatabaseFile (); + break; + + case MSG_DATABASE_NAME: + if (strcmp (m_DatabaseFileNameCachedValue, + m_DatabaseFileNameTextboxPntr->Text ()) != 0) + SubmitCommandString (PN_DATABASE_FILE, B_SET_PROPERTY, + m_DatabaseFileNameTextboxPntr->Text ()); + break; + + case MSG_ESTIMATE_BUTTON: + BrowseForFileToEstimate (); + break; + + case MSG_ESTIMATE_FILE_REFS: + EstimateRefFilesAndDisplay (MessagePntr); + break; + + case MSG_IGNORE_CLASSIFICATION: + TempBool = (m_IgnorePreviousClassCheckboxPntr->Value() == B_CONTROL_ON); + if (m_IgnorePreviousClassCachedValue != TempBool) + SubmitCommandBool (PN_IGNORE_PREVIOUS_CLASSIFICATION, + B_SET_PROPERTY, TempBool); + break; + + case MSG_PURGE_AGE: + TempUint32 = strtoul (m_PurgeAgeTextboxPntr->Text (), NULL, 10); + if (m_PurgeAgeCachedValue != TempUint32) + SubmitCommandInt32 (PN_PURGE_AGE, B_SET_PROPERTY, TempUint32); + break; + + case MSG_PURGE_POPULARITY: + TempUint32 = strtoul (m_PurgePopularityTextboxPntr->Text (), NULL, 10); + if (m_PurgePopularityCachedValue != TempUint32) + SubmitCommandInt32 (PN_PURGE_POPULARITY, B_SET_PROPERTY, TempUint32); + break; + + case MSG_SERVER_MODE: + TempBool = (m_ServerModeCheckboxPntr->Value() == B_CONTROL_ON); + if (m_ServerModeCachedValue != TempBool) + SubmitCommandBool (PN_SERVER_MODE, B_SET_PROPERTY, TempBool); + break; + + default: + BView::MessageReceived (MessagePntr); + } +} + + +/* Check the server for changes in the state of the database, and if there are +any changes, update the displayed values. Since this is a read only +examination of the server, we go directly to the application rather than +sending it messages. Also, when sending messages, we can't find out what it is +doing while it is busy with a batch of spam additions (all the spam add +commands will be in the queue ahead of our requests for info). Instead, we +lock the BApplication (so it isn't changing things while we're looking) and +retrieve our values. */ + +void ControlsView::PollServerForChanges () +{ + ABSApp *MyAppPntr; + BMenuItem *TempMenuItemPntr; + char TempString [PATH_MAX]; + BWindow *WindowPntr; + + /* We need a pointer to our window, for changing the title etc. */ + + WindowPntr = Window (); + if (WindowPntr == NULL) + return; /* No window, no point in updating the display! */ + + /* Check the server mode flag. If the mode is off, then the window has to be + minimized. Similarly, if it gets turned on, maximize the window. Note that + the user can maximize the window manually, even while still in server mode. + */ + + if (g_ServerMode != m_ServerModeCachedValue && + m_ServerModeCheckboxPntr != NULL) + { + m_ServerModeCachedValue = g_ServerMode; + m_ServerModeCheckboxPntr->SetValue ( + m_ServerModeCachedValue ? B_CONTROL_ON : B_CONTROL_OFF); + WindowPntr->Minimize (m_ServerModeCachedValue); + } + + if (WindowPntr->IsMinimized ()) + return; /* Window isn't visible, don't waste time updating it. */ + + /* So that people don't stare at a blank screen, request a database load if + nothing is there. But only do it once, so the user doesn't get a lot of + invalid database messages if one doesn't exist yet. In server mode, we never + get this far so it is only loaded when the user wants to see something. */ + + if (!m_DatabaseLoadDone) + { + m_DatabaseLoadDone = true; + /* Counting the number of words will load the database. */ + SubmitCommandString (PN_DATABASE_FILE, B_COUNT_PROPERTIES, ""); + } + + /* Check various read only values, which can be read from the BApplication + without having to lock it. This is useful for displaying the number of words + as it is changing. First up is the purge age setting. */ + + MyAppPntr = dynamic_cast (be_app); + if (MyAppPntr == NULL) + return; /* Doesn't exist or is the wrong class. Not likely! */ + + if (MyAppPntr->m_PurgeAge != m_PurgeAgeCachedValue && + m_PurgeAgeTextboxPntr != NULL) + { + m_PurgeAgeCachedValue = MyAppPntr->m_PurgeAge; + sprintf (TempString, "%lu", m_PurgeAgeCachedValue); + m_PurgeAgeTextboxPntr->SetText (TempString); + } + + /* Check the purge popularity. */ + + if (MyAppPntr->m_PurgePopularity != m_PurgePopularityCachedValue && + m_PurgePopularityTextboxPntr != NULL) + { + m_PurgePopularityCachedValue = MyAppPntr->m_PurgePopularity; + sprintf (TempString, "%lu", m_PurgePopularityCachedValue); + m_PurgePopularityTextboxPntr->SetText (TempString); + } + + /* Check the Ignore Previous Classification flag. */ + + if (MyAppPntr->m_IgnorePreviousClassification != + m_IgnorePreviousClassCachedValue && + m_IgnorePreviousClassCheckboxPntr != NULL) + { + m_IgnorePreviousClassCachedValue = + MyAppPntr->m_IgnorePreviousClassification; + m_IgnorePreviousClassCheckboxPntr->SetValue ( + m_IgnorePreviousClassCachedValue ? B_CONTROL_ON : B_CONTROL_OFF); + } + + /* Update the genuine count. */ + + if (MyAppPntr->m_TotalGenuineMessages != m_GenuineCountCachedValue && + m_GenuineCountTextboxPntr != NULL) + { + m_GenuineCountCachedValue = MyAppPntr->m_TotalGenuineMessages; + sprintf (TempString, "%lu", m_GenuineCountCachedValue); + m_GenuineCountTextboxPntr->SetText (TempString); + } + + /* Update the spam count. */ + + if (MyAppPntr->m_TotalSpamMessages != m_SpamCountCachedValue && + m_SpamCountTextboxPntr != NULL) + { + m_SpamCountCachedValue = MyAppPntr->m_TotalSpamMessages; + sprintf (TempString, "%lu", m_SpamCountCachedValue); + m_SpamCountTextboxPntr->SetText (TempString); + } + + /* Update the word count. */ + + if (MyAppPntr->m_WordCount != m_WordCountCachedValue && + m_WordCountTextboxPntr != NULL) + { + m_WordCountCachedValue = MyAppPntr->m_WordCount; + sprintf (TempString, "%lu", m_WordCountCachedValue); + m_WordCountTextboxPntr->SetText (TempString); + } + + /* Update the tokenize mode pop-up menu. */ + + if (MyAppPntr->m_TokenizeMode != m_TokenizeModeCachedValue && + m_TokenizeModePopUpMenuPntr != NULL) + { + m_TokenizeModeCachedValue = MyAppPntr->m_TokenizeMode; + TempMenuItemPntr = + m_TokenizeModePopUpMenuPntr->ItemAt ((int) m_TokenizeModeCachedValue); + if (TempMenuItemPntr != NULL) + TempMenuItemPntr->SetMarked (true); + } + + /* Update the scoring mode pop-up menu. */ + + if (MyAppPntr->m_ScoringMode != m_ScoringModeCachedValue && + m_ScoringModePopUpMenuPntr != NULL) + { + m_ScoringModeCachedValue = MyAppPntr->m_ScoringMode; + TempMenuItemPntr = + m_ScoringModePopUpMenuPntr->ItemAt ((int) m_ScoringModeCachedValue); + if (TempMenuItemPntr != NULL) + TempMenuItemPntr->SetMarked (true); + } + + /* Lock the application. This will stop it from processing any further + messages until we are done. Or if it is busy, the lock will fail. */ + + if (MyAppPntr->LockWithTimeout (100000) != B_OK) + return; /* It's probably busy doing something. */ + + /* See if the database file name has changed. */ + + if (strcmp (MyAppPntr->m_DatabaseFileName.String (), + m_DatabaseFileNameCachedValue) != 0 && + m_DatabaseFileNameTextboxPntr != NULL) + { + strcpy (m_DatabaseFileNameCachedValue, + MyAppPntr->m_DatabaseFileName.String ()); + m_DatabaseFileNameTextboxPntr->SetText (m_DatabaseFileNameCachedValue); + WindowPntr->SetTitle (m_DatabaseFileNameCachedValue); + } + + /* Done. Let the BApplication continue processing messages. */ + + MyAppPntr->Unlock (); +} + + +void ControlsView::Pulse () +{ + if (system_time () > m_TimeOfLastPoll + 200000) + { + PollServerForChanges (); + m_TimeOfLastPoll = system_time (); + } +} + + + +/****************************************************************************** + * Implementation of the DatabaseWindow class, constructor, destructor and the + * rest of the member functions in mostly alphabetical order. + */ + +DatabaseWindow::DatabaseWindow () +: BWindow (BRect (30, 30, 620, 400), + "Alexander G. M. Smith's Bayesian Spam Database Server", + B_DOCUMENT_WINDOW, B_ASYNCHRONOUS_CONTROLS) +{ + BRect TempRect; + + /* Add the controls view. */ + + m_ControlsViewPntr = new ControlsView (Bounds ()); + if (m_ControlsViewPntr == NULL) + goto ErrorExit; + AddChild (m_ControlsViewPntr); + + /* Add the word view in the remaining space under the controls view. */ + + TempRect = Bounds (); + TempRect.top = m_ControlsViewPntr->Frame().bottom + 1; + m_WordsViewPntr = new WordsView (TempRect); + if (m_WordsViewPntr == NULL) + goto ErrorExit; + AddChild (m_WordsViewPntr); + + /* Minimize the window if we are starting up in server mode. This is done + before the window is open so it doesn't flash onto the screen, and possibly + steal a keystroke or two. The ControlsView will further update the minimize + mode when it detects changes in the server mode. */ + + Minimize (g_ServerMode); + + return; + +ErrorExit: + DisplayErrorMessage ("Unable to initialise the window contents."); +} + + +void DatabaseWindow::MessageReceived (BMessage *MessagePntr) +{ + if (MessagePntr->what == B_MOUSE_WHEEL_CHANGED) + { + /* Pass the mouse wheel stuff down to the words view, since that's the only + one which does scrolling so we don't need to worry about whether it has + focus or not. */ + + if (m_WordsViewPntr != NULL) + m_WordsViewPntr->MessageReceived (MessagePntr); + } + else + BWindow::MessageReceived (MessagePntr); +} + + +bool DatabaseWindow::QuitRequested () +{ + be_app->PostMessage (B_QUIT_REQUESTED); + return true; +} + + + +/****************************************************************************** + * Implementation of the word display view. + */ + +WordsView::WordsView (BRect NewBounds) +: BView (NewBounds, "WordsView", B_FOLLOW_ALL_SIDES, + B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_NAVIGABLE | B_PULSE_NEEDED), + m_ArrowLineDownPntr (NULL), + m_ArrowLineUpPntr (NULL), + m_ArrowPageDownPntr (NULL), + m_ArrowPageUpPntr (NULL), + m_LastTimeAKeyWasPressed (0) +{ + font_height TempFontHeight; + + GetFont (&m_TextFont); /* Modify the default font to be our own. */ + m_TextFont.SetSize (ceilf (m_TextFont.Size() * 1.1)); + m_TextFont.GetHeight (&TempFontHeight); + SetFont (&m_TextFont); + + m_LineHeight = ceilf (TempFontHeight.ascent + + TempFontHeight.descent + TempFontHeight.leading); + m_AscentHeight = ceilf (TempFontHeight.ascent); + m_TextHeight = ceilf (TempFontHeight.ascent + + TempFontHeight.descent); + + m_FocusedColour.red = 255; + m_FocusedColour.green = 255; + m_FocusedColour.blue = 255; + m_FocusedColour.alpha = 255; + + m_UnfocusedColour.red = 245; + m_UnfocusedColour.green = 245; + m_UnfocusedColour.blue = 255; + m_UnfocusedColour.alpha = 255; + + m_BackgroundColour = m_UnfocusedColour; + SetViewColor (m_BackgroundColour); + SetLowColor (m_BackgroundColour); + SetHighColor (0, 0, 0); + + strcpy (m_FirstDisplayedWord, "a"); +} + + +void WordsView::AttachedToWindow () +{ + BPolygon DownLinePolygon (g_DownLinePoints, + sizeof (g_DownLinePoints) / + sizeof (g_DownLinePoints[0])); + + BPolygon DownPagePolygon (g_DownPagePoints, + sizeof (g_DownPagePoints) / + sizeof (g_DownPagePoints[0])); + + BPolygon UpLinePolygon (g_UpLinePoints, + sizeof (g_UpLinePoints) / + sizeof (g_UpLinePoints[0])); + + BPolygon UpPagePolygon (g_UpPagePoints, + sizeof (g_UpPagePoints) / + sizeof (g_UpPagePoints[0])); + + BPicture TempOffPicture; + BPicture TempOnPicture; + BRect TempRect; + + /* Make the buttons and associated polygon images for the forward and + backwards a word or a page of words buttons. They're the width of the scroll + bar area on the right, but twice as tall as usual, since there is no scroll + bar and that will make it easier to use them. First the up a line button. */ + + SetHighColor (0, 0, 0); + BeginPicture (&TempOffPicture); + FillPolygon (&UpLinePolygon); + SetHighColor (180, 180, 180); + StrokePolygon (&UpLinePolygon); + EndPicture (); + + SetHighColor (128, 128, 128); + BeginPicture (&TempOnPicture); + FillPolygon (&UpLinePolygon); + EndPicture (); + + TempRect = Bounds (); + TempRect.bottom = TempRect.top + 2 * B_H_SCROLL_BAR_HEIGHT; + TempRect.left = TempRect.right - B_V_SCROLL_BAR_WIDTH; + m_ArrowLineUpPntr = new BPictureButton (TempRect, "Up Line", + &TempOffPicture, &TempOnPicture, + new BMessage (MSG_LINE_UP), B_ONE_STATE_BUTTON, + B_FOLLOW_RIGHT | B_FOLLOW_TOP, B_WILL_DRAW | B_NAVIGABLE); + if (m_ArrowLineUpPntr == NULL) goto ErrorExit; + AddChild (m_ArrowLineUpPntr); + m_ArrowLineUpPntr->SetTarget (this); + + /* Up a page button. */ + + SetHighColor (0, 0, 0); + BeginPicture (&TempOffPicture); + FillPolygon (&UpPagePolygon); + SetHighColor (180, 180, 180); + StrokePolygon (&UpPagePolygon); + EndPicture (); + + SetHighColor (128, 128, 128); + BeginPicture (&TempOnPicture); + FillPolygon (&UpPagePolygon); + EndPicture (); + + TempRect = Bounds (); + TempRect.top += 2 * B_H_SCROLL_BAR_HEIGHT + 1; + TempRect.bottom = TempRect.top + 2 * B_H_SCROLL_BAR_HEIGHT; + TempRect.left = TempRect.right - B_V_SCROLL_BAR_WIDTH; + m_ArrowPageUpPntr = new BPictureButton (TempRect, "Up Page", + &TempOffPicture, &TempOnPicture, + new BMessage (MSG_PAGE_UP), B_ONE_STATE_BUTTON, + B_FOLLOW_RIGHT | B_FOLLOW_TOP, B_WILL_DRAW | B_NAVIGABLE); + if (m_ArrowPageUpPntr == NULL) goto ErrorExit; + AddChild (m_ArrowPageUpPntr); + m_ArrowPageUpPntr->SetTarget (this); + + /* Down a page button. */ + + SetHighColor (0, 0, 0); + BeginPicture (&TempOffPicture); + FillPolygon (&DownPagePolygon); + SetHighColor (180, 180, 180); + StrokePolygon (&DownPagePolygon); + EndPicture (); + + SetHighColor (128, 128, 128); + BeginPicture (&TempOnPicture); + FillPolygon (&DownPagePolygon); + EndPicture (); + + TempRect = Bounds (); + TempRect.bottom -= 3 * B_H_SCROLL_BAR_HEIGHT + 1; + TempRect.top = TempRect.bottom - 2 * B_H_SCROLL_BAR_HEIGHT; + TempRect.left = TempRect.right - B_V_SCROLL_BAR_WIDTH; + m_ArrowPageDownPntr = new BPictureButton (TempRect, "Down Page", + &TempOffPicture, &TempOnPicture, + new BMessage (MSG_PAGE_DOWN), B_ONE_STATE_BUTTON, + B_FOLLOW_RIGHT | B_FOLLOW_BOTTOM, B_WILL_DRAW | B_NAVIGABLE); + if (m_ArrowPageDownPntr == NULL) goto ErrorExit; + AddChild (m_ArrowPageDownPntr); + m_ArrowPageDownPntr->SetTarget (this); + + /* Down a line button. */ + + SetHighColor (0, 0, 0); + BeginPicture (&TempOffPicture); + FillPolygon (&DownLinePolygon); + SetHighColor (180, 180, 180); + StrokePolygon (&DownLinePolygon); + EndPicture (); + + SetHighColor (128, 128, 128); + BeginPicture (&TempOnPicture); + FillPolygon (&DownLinePolygon); + EndPicture (); + + TempRect = Bounds (); + TempRect.bottom -= B_H_SCROLL_BAR_HEIGHT; + TempRect.top = TempRect.bottom - 2 * B_H_SCROLL_BAR_HEIGHT; + TempRect.left = TempRect.right - B_V_SCROLL_BAR_WIDTH; + m_ArrowLineDownPntr = new BPictureButton (TempRect, "Down Line", + &TempOffPicture, &TempOnPicture, + new BMessage (MSG_LINE_DOWN), B_ONE_STATE_BUTTON, + B_FOLLOW_RIGHT | B_FOLLOW_BOTTOM, B_WILL_DRAW | B_NAVIGABLE); + if (m_ArrowLineDownPntr == NULL) goto ErrorExit; + AddChild (m_ArrowLineDownPntr); + m_ArrowLineDownPntr->SetTarget (this); + + return; + +ErrorExit: + DisplayErrorMessage ("Problems while making view displaying the words."); +} + + +/* Draw the words starting with the one at or after m_FirstDisplayedWord. This +requires looking at the database in the BApplication, which may or may not be +available (if it isn't, don't draw, a redraw will usually be requested by the +Pulse member function when it keeps on noticing that the stuff on the display +doesn't match the database). */ + +void WordsView::Draw (BRect UpdateRect) +{ + float AgeDifference; + float AgeProportion; + float CenterX; + float ColumnLeftCenterX; + float ColumnMiddleCenterX; + float ColumnRightCenterX; + float CompensatedRatio; + StatisticsMap::iterator DataIter; + StatisticsMap::iterator EndIter; + rgb_color FillColour; + float GenuineProportion; + uint32 GenuineSpamSum; + float HeightPixels; + float HeightProportion; + float LeftBounds; + ABSApp *MyAppPntr; + uint32 NewestAge; + uint32 OldestAge; + float OneFifthTotalGenuine; + float OneFifthTotalSpam; + double RawProbabilityRatio; + float RightBounds; + float SpamProportion; + StatisticsPointer StatisticsPntr; + BRect TempRect; + char TempString [PATH_MAX]; + float TotalGenuineMessages = 1.0; /* Avoid divide by 0. */ + float TotalSpamMessages = 1.0; + float Width; + float Y; + + /* Lock the application. This will stop it from processing any further + messages until we are done. Or if it is busy, the lock will fail. */ + + MyAppPntr = dynamic_cast (be_app); + if (MyAppPntr == NULL || MyAppPntr->LockWithTimeout (100000) != B_OK) + return; /* It's probably busy doing something. */ + + /* Set up various loop invariant variables. */ + + if (MyAppPntr->m_TotalGenuineMessages > 0) + TotalGenuineMessages = MyAppPntr->m_TotalGenuineMessages; + OneFifthTotalGenuine = TotalGenuineMessages / 5; + + if (MyAppPntr->m_TotalSpamMessages > 0) + TotalSpamMessages = MyAppPntr->m_TotalSpamMessages; + OneFifthTotalSpam = TotalSpamMessages / 5; + + EndIter = MyAppPntr->m_WordMap.end (); + + OldestAge = MyAppPntr->m_OldestAge; + NewestAge = /* actually newest age plus one */ + MyAppPntr->m_TotalGenuineMessages + MyAppPntr->m_TotalSpamMessages; + + if (NewestAge == 0) + goto NormalExit; /* No words to display, or something is badly wrong. */ + + NewestAge--; /* The newest message has age NewestAge. */ + AgeDifference = NewestAge - OldestAge; /* Can be zero if just one message. */ + + LeftBounds = Bounds().left; + RightBounds = Bounds().right - B_V_SCROLL_BAR_WIDTH; + Width = RightBounds - LeftBounds; + FillColour.alpha = 255; + + CenterX = ceilf (LeftBounds + Width * 0.5); + ColumnLeftCenterX = ceilf (LeftBounds + Width * 0.05); + ColumnMiddleCenterX = CenterX; + ColumnRightCenterX = ceilf (LeftBounds + Width * 0.95); + + for (DataIter = MyAppPntr->m_WordMap.lower_bound (m_FirstDisplayedWord), + Y = Bounds().top; + DataIter != EndIter && Y < UpdateRect.bottom; + DataIter++, Y += m_LineHeight) + { + if (Y + m_LineHeight < UpdateRect.top) + continue; /* Not in the visible area yet, don't actually draw. */ + + /* Draw the colour bar behind the word. It reflects the spamness or + genuineness of that particular word, plus the importance of the word and + the age of the word. + + First calculate the compensated spam ratio (described elsewhere). It is + close to 0.0 for genuine words and close to 1.0 for pure spam. It is drawn + as a blue bar to the left of center if it is less than 0.5, and a red bar + on the right of center if it is greater than 0.5. At exactly 0.5 nothing + is drawn; the word is worthless as an indicator. + + The height of the bar corresponds to the number of messages the word was + found in. Make the height proportional to the total of spam and genuine + messages for the word divided by the sum of the most extreme spam and + genuine counts in the database. + + The staturation of the colour corresponds to the age of the word, with old + words being almost white rather than solid blue or red. */ + + StatisticsPntr = &DataIter->second; + + SpamProportion = StatisticsPntr->spamCount / TotalSpamMessages; + GenuineProportion = StatisticsPntr->genuineCount / TotalGenuineMessages; + if (SpamProportion + GenuineProportion > 0.0f) + RawProbabilityRatio = + SpamProportion / (SpamProportion + GenuineProportion); + else + RawProbabilityRatio = g_RobinsonX; + + /* The compensated ratio leans towards 0.5 (RobinsonX) more for fewer + data points, with a weight of 0.45 (RobinsonS). */ + + GenuineSpamSum = + StatisticsPntr->spamCount + StatisticsPntr->genuineCount; + CompensatedRatio = + (g_RobinsonS * g_RobinsonX + GenuineSpamSum * RawProbabilityRatio) / + (g_RobinsonS + GenuineSpamSum); + + /* Used to use the height based on the most frequent word, but some words, + like "From", show up in all messages which made most other words just + appear as a thin line. I did a histogram plot of the sizes in my test + database, and figured that you get better coverage of 90% of the messages + if you use 1/5 of the total number as the count which gives you 100% + height. The other 10% get a full height bar, but most people wouldn't care + that they're super frequently used. */ + + HeightProportion = 0.5f * (StatisticsPntr->genuineCount / + OneFifthTotalGenuine + StatisticsPntr->spamCount / OneFifthTotalSpam); + + if (HeightProportion > 1.0f) + HeightProportion = 1.0f; + HeightPixels = ceilf (HeightProportion * m_TextHeight); + + if (AgeDifference <= 0.0f) + AgeProportion = 1.0; /* New is 1.0, old is 0.0 */ + else + AgeProportion = (StatisticsPntr->age - OldestAge) / AgeDifference; + + TempRect.top = ceilf (Y + m_TextHeight / 2 - HeightPixels / 2); + TempRect.bottom = TempRect.top + HeightPixels; + + if (CompensatedRatio < 0.5f) + { + TempRect.left = ceilf ( + CenterX - 1.6f * (0.5f - CompensatedRatio) * (CenterX - LeftBounds)); + TempRect.right = CenterX; + FillColour.red = 230 - (int) (AgeProportion * 230.0f); + FillColour.green = FillColour.red; + FillColour.blue = 255; + } + else /* Ratio >= 0.5, red spam block. */ + { + TempRect.left = CenterX; + TempRect.right = ceilf ( + CenterX + 1.6f * (CompensatedRatio - 0.5f) * (RightBounds - CenterX)); + FillColour.blue = 230 - (int) (AgeProportion * 230.0f); + FillColour.green = FillColour.blue; + FillColour.red = 255; + } + SetHighColor (FillColour); + SetDrawingMode (B_OP_COPY); + FillRect (TempRect); + + /* Print the text centered in columns of various widths. The number of + genuine messages in the left 10% of the width, the word in the middle 80%, + and the number of spam messages using the word in the right 10%. */ + + SetHighColor (0, 0, 0); + SetDrawingMode (B_OP_OVER); /* So that antialiased text mixes better. */ + + sprintf (TempString, "%lu", StatisticsPntr->genuineCount); + Width = m_TextFont.StringWidth (TempString); + MovePenTo (ceilf (ColumnLeftCenterX - Width / 2), Y + m_AscentHeight); + DrawString (TempString); + + strcpy (TempString, DataIter->first.c_str ()); + Width = m_TextFont.StringWidth (TempString); + MovePenTo (ceilf (ColumnMiddleCenterX - Width / 2), Y + m_AscentHeight); + DrawString (TempString); + + sprintf (TempString, "%lu", StatisticsPntr->spamCount); + Width = m_TextFont.StringWidth (TempString); + MovePenTo (ceilf (ColumnRightCenterX - Width / 2), Y + m_AscentHeight); + DrawString (TempString); + } + + /* Draw the first word (the one which the user types in to select the first + displayed word) on the right, in the scroll bar margin, rotated 90 degrees to + fit between the page up and page down buttons. */ + + Width = m_TextFont.StringWidth (m_FirstDisplayedWord); + if (Width > 0) + { + TempRect = Bounds (); + TempRect.top += 4 * B_H_SCROLL_BAR_HEIGHT + 1; + TempRect.bottom -= 5 * B_H_SCROLL_BAR_HEIGHT + 1; + + MovePenTo (TempRect.right - m_TextHeight + m_AscentHeight - 1, + ceilf ((TempRect.bottom + TempRect.top) / 2 + Width / 2)); + m_TextFont.SetRotation (90); + SetFont (&m_TextFont, B_FONT_ROTATION); + DrawString (m_FirstDisplayedWord); + m_TextFont.SetRotation (0); + SetFont (&m_TextFont, B_FONT_ROTATION); + } + +NormalExit: + + /* Successfully finished drawing. Update the cached values to match what we + have drawn. */ + m_CachedTotalGenuineMessages = MyAppPntr->m_TotalGenuineMessages; + m_CachedTotalSpamMessages = MyAppPntr->m_TotalSpamMessages; + m_CachedWordCount = MyAppPntr->m_WordCount; + + /* Done. Let the BApplication continue processing messages. */ + MyAppPntr->Unlock (); +} + + +/* When the user presses keys, they select the first word to be displayed in +the view (it's the word at or lexicographically after the word typed in). The +keys are appended to the starting word, until the user stops typing for a +while, then the next key will be the first letter of a new starting word. */ + +void WordsView::KeyDown (const char *BufferPntr, int32 NumBytes) +{ + int32 CharLength; + bigtime_t CurrentTime; + char TempString [40]; + + CurrentTime = system_time (); + + if (NumBytes < (int32) sizeof (TempString)) + { + memcpy (TempString, BufferPntr, NumBytes); + TempString [NumBytes] = 0; + CharLength = strlen (TempString); /* So NUL bytes don't get through. */ + + /* Check for arrow keys, which move the view up and down. */ + + if (CharLength == 1 && + (TempString[0] == B_UP_ARROW || + TempString[0] == B_DOWN_ARROW || + TempString[0] == B_PAGE_UP || + TempString[0] == B_PAGE_DOWN)) + { + MoveTextUpOrDown ((TempString[0] == B_UP_ARROW) ? MSG_LINE_UP : + ((TempString[0] == B_DOWN_ARROW) ? MSG_LINE_DOWN : + ((TempString[0] == B_PAGE_UP) ? MSG_PAGE_UP : MSG_PAGE_DOWN))); + } + else if (CharLength > 1 || + (CharLength == 1 && 32 <= (uint8) TempString[0])) + { + /* Have a non-control character, or some sort of multibyte char. Add it + to the word and mark things for redisplay starting at the resulting word. + */ + + if (CurrentTime - m_LastTimeAKeyWasPressed >= 1000000 /* microseconds */) + strcpy (m_FirstDisplayedWord, TempString); /* Starting a new word. */ + else if (strlen (m_FirstDisplayedWord) + CharLength <= g_MaxWordLength) + strcat (m_FirstDisplayedWord, TempString); /* Append to existing. */ + + Invalidate (); + } + } + + m_LastTimeAKeyWasPressed = CurrentTime; + BView::KeyDown (BufferPntr, NumBytes); +} + + +/* Change the background colour to show that we have the focus. When we have +it, keystrokes will select the word to be displayed at the top of the list. */ + +void WordsView::MakeFocus (bool Focused) +{ + if (Focused) + m_BackgroundColour = m_FocusedColour; + else + m_BackgroundColour = m_UnfocusedColour; + SetViewColor (m_BackgroundColour); + SetLowColor (m_BackgroundColour); + + /* Also need to set the background colour for the scroll buttons, since they + can't be made transparent. */ + + if (m_ArrowLineDownPntr != NULL) + { + m_ArrowLineDownPntr->SetViewColor (m_BackgroundColour); + m_ArrowLineDownPntr->Invalidate (); + } + + if (m_ArrowLineUpPntr != NULL) + { + m_ArrowLineUpPntr->SetViewColor (m_BackgroundColour); + m_ArrowLineUpPntr->Invalidate (); + } + + if (m_ArrowPageDownPntr != NULL) + { + m_ArrowPageDownPntr->SetViewColor (m_BackgroundColour); + m_ArrowPageDownPntr->Invalidate (); + } + + if (m_ArrowPageUpPntr != NULL) + { + m_ArrowPageUpPntr->SetViewColor (m_BackgroundColour); + m_ArrowPageUpPntr->Invalidate (); + } + + Invalidate (); + + BView::MakeFocus (Focused); +} + + +void WordsView::MessageReceived (BMessage *MessagePntr) +{ + int32 CountFound; + float DeltaY; /* Usually -1.0, 0.0 or +1.0. */ + type_code TypeFound; + + switch (MessagePntr->what) + { + case B_MOUSE_WHEEL_CHANGED: + if (MessagePntr->FindFloat ("be:wheel_delta_y", &DeltaY) != 0) break; + if (DeltaY < 0) + MoveTextUpOrDown (MSG_LINE_UP); + else if (DeltaY > 0) + MoveTextUpOrDown (MSG_LINE_DOWN); + break; + + case MSG_LINE_DOWN: + case MSG_LINE_UP: + case MSG_PAGE_DOWN: + case MSG_PAGE_UP: + MoveTextUpOrDown (MessagePntr->what); + break; + + case B_SIMPLE_DATA: /* Something has been dropped in our view. */ + if (MessagePntr->GetInfo ("refs", &TypeFound, &CountFound) == B_OK && + CountFound > 0 && TypeFound == B_REF_TYPE) + { + RefsDroppedHere (MessagePntr); + break; + } + /* Else fall through to the default case, in case it is something else + dropped that the system knows about. */ + + default: + BView::MessageReceived (MessagePntr); + } +} + + +/* If the user clicks on our view, take over the focus. */ + +void WordsView::MouseDown (BPoint) +{ + if (!IsFocus ()) + MakeFocus (true); +} + + +void WordsView::MoveTextUpOrDown (uint32 MovementType) +{ + StatisticsMap::iterator DataIter; + int i; + ABSApp *MyAppPntr; + int PageSize; + + /* Lock the application. This will stop it from processing any further + messages until we are done (we need to look at the word list directly). Or + if it is busy, the lock will fail. */ + + MyAppPntr = dynamic_cast (be_app); + if (MyAppPntr == NULL || MyAppPntr->LockWithTimeout (2000000) != B_OK) + return; /* It's probably busy doing something. */ + + PageSize = (int) (Bounds().Height() / m_LineHeight - 1); + if (PageSize < 1) + PageSize = 1; + + DataIter = MyAppPntr->m_WordMap.lower_bound (m_FirstDisplayedWord); + + switch (MovementType) + { + case MSG_LINE_UP: + if (DataIter != MyAppPntr->m_WordMap.begin ()) + DataIter--; + break; + + case MSG_LINE_DOWN: + if (DataIter != MyAppPntr->m_WordMap.end ()) + DataIter++; + break; + + case MSG_PAGE_UP: + for (i = 0; i < PageSize; i++) + { + if (DataIter == MyAppPntr->m_WordMap.begin ()) + break; + DataIter--; + } + break; + + case MSG_PAGE_DOWN: + for (i = 0; i < PageSize; i++) + { + if (DataIter == MyAppPntr->m_WordMap.end ()) + break; + DataIter++; + } + break; + } + + if (DataIter != MyAppPntr->m_WordMap.end ()) + strcpy (m_FirstDisplayedWord, DataIter->first.c_str ()); + + Invalidate (); + + MyAppPntr->Unlock (); +} + + +/* This function periodically polls the BApplication to see if anything has +changed. If the word list is different or the display has changed in some +other way, it will then try to refresh the display, repeating the attempt until +it gets successfully drawn. */ + +void WordsView::Pulse () +{ + ABSApp *MyAppPntr; + + /* Probe the BApplication to see if it has changed. */ + + MyAppPntr = dynamic_cast (be_app); + if (MyAppPntr == NULL) + return; /* Something is wrong, give up. */ + + if (MyAppPntr->m_TotalGenuineMessages != m_CachedTotalGenuineMessages || + MyAppPntr->m_TotalSpamMessages != m_CachedTotalSpamMessages || + MyAppPntr->m_WordCount != m_CachedWordCount) + Invalidate (); +} + + +/* The user has dragged and dropped some file references on the words view. If +it is in the left third, add the file(s) as examples of genuine messages, right +third for spam messages and if it is in the middle third then evaluate the +file(s) for spaminess. */ + +void WordsView::RefsDroppedHere (BMessage *MessagePntr) +{ + float Left; + bool SpamExample = true; /* TRUE if example is of spam, FALSE genuine. */ + float Third; + BPoint WhereDropped; + + /* Find out which third of the view it was dropped into. */ + + if (MessagePntr->FindPoint ("_drop_point_", &WhereDropped) != B_OK) + return; /* Need to know where it was dropped. */ + ConvertFromScreen (&WhereDropped); + Third = Bounds().Width() / 3; + Left = Bounds().left; + if (WhereDropped.x < Left + Third) + SpamExample = false; + else if (WhereDropped.x < Left + 2 * Third) + { + /* In the middle third, evaluate all files for spaminess. */ + EstimateRefFilesAndDisplay (MessagePntr); + return; + } + + if (g_CommanderLooperPntr != NULL) + g_CommanderLooperPntr->CommandReferences ( + MessagePntr, true /* BulkMode */, SpamExample ? CL_SPAM : CL_GENUINE); +} + + + +/****************************************************************************** + * Finally, the main program which drives it all. + */ + +int main (int argc, char**) +{ + g_CommandLineMode = (argc > 1); + if (!g_CommandLineMode) + cout << PrintUsage; /* In case no arguments specified. */ + + g_CommanderLooperPntr = new CommanderLooper; + if (g_CommanderLooperPntr != NULL) + { + g_CommanderMessenger = new BMessenger (NULL, g_CommanderLooperPntr); + g_CommanderLooperPntr->Run (); + } + + ABSApp MyApp; + + if (MyApp.InitCheck () == 0) + { + MyApp.LoadSaveSettings (true /* DoLoad */); + MyApp.Run (); + } + + if (g_CommanderLooperPntr != NULL) + { + g_CommanderLooperPntr->PostMessage (B_QUIT_REQUESTED); + snooze (100000); /* Let the CommanderLooper thread run so it quits. */ + } + + cerr << "AGMSBayesianSpamServer shutting down..." << endl; + return 0; /* And implicitly destroys MyApp, which writes out the database. */ +}