From 39d27bcb29468cdb6231555e8b1239ddc184112a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sundstr=C3=B6m?= Date: Sat, 19 Sep 2009 21:00:23 +0000 Subject: [PATCH] Renaming mail-mbox utilities as per humdingers request in #4582. git-svn-id: file:///srv/svn/repos/haiku/haiku/trunk@33195 a95241bf-73f2-0310-859d-f6bbb57e9c96 --- src/bin/Jamfile | 2 +- src/bin/bemail_utils/Jamfile | 36 - src/bin/bemail_utils/mail2mbox.cpp | 446 -- src/bin/bemail_utils/mail2mbox.rdef | 70 - src/bin/bemail_utils/mbox2mail.cpp | 757 --- src/bin/bemail_utils/mbox2mail.rdef | 71 - src/bin/bemail_utils/spamdbm.cpp | 7797 --------------------------- src/bin/bemail_utils/spamdbm.rdef | 136 - 8 files changed, 1 insertion(+), 9314 deletions(-) delete mode 100644 src/bin/bemail_utils/Jamfile delete mode 100644 src/bin/bemail_utils/mail2mbox.cpp delete mode 100644 src/bin/bemail_utils/mail2mbox.rdef delete mode 100644 src/bin/bemail_utils/mbox2mail.cpp delete mode 100644 src/bin/bemail_utils/mbox2mail.rdef delete mode 100644 src/bin/bemail_utils/spamdbm.cpp delete mode 100644 src/bin/bemail_utils/spamdbm.rdef diff --git a/src/bin/Jamfile b/src/bin/Jamfile index e0feb7f200..1689cac87e 100644 --- a/src/bin/Jamfile +++ b/src/bin/Jamfile @@ -191,7 +191,7 @@ StdBinCommands SubInclude HAIKU_TOP src bin addattr ; SubInclude HAIKU_TOP src bin bash ; SubInclude HAIKU_TOP src bin bc ; -SubInclude HAIKU_TOP src bin bemail_utils ; +SubInclude HAIKU_TOP src bin mail_utils ; SubInclude HAIKU_TOP src bin compress ; SubInclude HAIKU_TOP src bin coreutils ; SubInclude HAIKU_TOP src bin ctags ; diff --git a/src/bin/bemail_utils/Jamfile b/src/bin/bemail_utils/Jamfile deleted file mode 100644 index 942af0c943..0000000000 --- a/src/bin/bemail_utils/Jamfile +++ /dev/null @@ -1,36 +0,0 @@ -SubDir HAIKU_TOP src bin mail_utils ; - -SetSubDirSupportedPlatformsBeOSCompatible ; - -if $(TARGET_PLATFORM) != haiku { - UsePublicHeaders mail ; -} - -UsePrivateHeaders mail ; - -StdBinCommands - mail2mbox.cpp - : be - : mail2mbox.rdef - ; -Package haiku-maildaemon-cvs : - mail2mbox : - boot home config bin ; - -StdBinCommands - mbox2mail.cpp - : be libmail.so - : mbox2mail.rdef - ; -Package haiku-maildaemon-cvs : - mbox2mail : - boot home config bin ; - -StdBinCommands - spamdbm.cpp - : be tracker $(TARGET_LIBSTDC++) libmail.so - : spamdbm.rdef - ; -Package haiku-maildaemon-cvs : - spamdbm : - boot home config bin ; diff --git a/src/bin/bemail_utils/mail2mbox.cpp b/src/bin/bemail_utils/mail2mbox.cpp deleted file mode 100644 index e36d6dc6f5..0000000000 --- a/src/bin/bemail_utils/mail2mbox.cpp +++ /dev/null @@ -1,446 +0,0 @@ -/****************************************************************************** - * $Id$ - * - * BeMailToMBox is a utility program (requested by Frank Zschockelt) that - * converts BeOS e-mail files into Unix mailbox files (the kind that Pine - * uses). All the files in the input directory are concatenated with the - * appropriate mbox header lines added between them, and trailing blank lines - * reduced. The resulting text is written to standard output. Command line - * driven. - * - * $Log: BeMailToMBox.cpp,v $ (now manually updated) - * r13960 | agmsmith | 2005-08-13 22:10:49 -0400 (Sat, 13 Aug 2005) | 3 lines - * More file movement for the BeMail utilities... Content updates later to - * avoid confusing svn. - * - * r13955 | agmsmith | 2005-08-13 19:43:41 -0400 (Sat, 13 Aug 2005) | 5 lines - * Half way through adding some more BeMail related utilities - they use - * libmail.so which isn't backwards compatibile so they need recompiling for - * Haiku - thus better to include them here. Also want spam levels of 1E-6 to - * be visible for genuine messages. - * - * Revision 1.1 2002/02/24 18:16:46 agmsmith - * Initial revision - */ - -/* BeOS headers. */ - -#include -#include -#include - -/* Posix headers. */ - -#include -#include -#include -#include -#include - - -/****************************************************************************** - * Globals - */ - -time_t DateStampTime; - /* Time value used for stamping each message header. Incremented by 1 second - for each message, starts out with the current local time. */ - - -/****************************************************************************** - * 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". - */ - -static void DisplayErrorMessage ( - const char *MessageString = NULL, - int ErrorNumber = 0, - const char *TitleString = NULL) -{ - char ErrorBuffer [B_PATH_NAME_LENGTH + 80 /* error message */ + 80]; - - if (TitleString == NULL) - TitleString = "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; - } - - fputs (TitleString, stderr); - fputc ('\n', stderr); - fputs (MessageString, stderr); - fputc ('\n', stderr); -} - - - -/****************************************************************************** - * Determine if a line of text is the start of another message. Pine mailbox - * files have messages that start with a line that could say something like - * "From agmsmith@achilles.net Fri Oct 31 21:19:36 EST 1997" or maybe something - * like "From POPmail Mon Oct 20 21:12:36 1997" or in a more modern format, - * "From agmsmith@achilles.net Tue Sep 4 09:04:11 2001 -0400". I generalise it - * to "From blah Day MMM NN XX:XX:XX TZONE1 YYYY TZONE2". Blah is an e-mail - * address you can ignore (just treat it as a word separated by spaces). Day - * is a 3 letter day of the week. MMM is a 3 letter month name. NN is the two - * digit day of the week, has a leading space if the day is less than 10. - * XX:XX:XX is the time, the X's are digits. TZONE1 is the old style optional - * time zone of 3 capital letters. YYYY is the four digit year. TZONE2 is the - * optional modern time zone info, a plus or minus sign and 4 digits. Returns - * true if the line of text (ended with a NUL byte, no line feed or carriage - * returns at the end) is the start of a message. - */ - -bool IsStartOfMailMessage (char *LineString) -{ - char *StringPntr; - - /* It starts with "From " */ - - if (memcmp ("From ", LineString, 5) != 0) - return false; - StringPntr = LineString + 4; - while (*StringPntr == ' ') - StringPntr++; - - /* Skip over the e-mail address (or stop at the end of string). */ - - while (*StringPntr != ' ' && *StringPntr != 0) - StringPntr++; - while (*StringPntr == ' ') - StringPntr++; - - /* Look for the 3 letter day of the week. */ - - if (memcmp (StringPntr, "Mon", 3) != 0 && - memcmp (StringPntr, "Tue", 3) != 0 && - memcmp (StringPntr, "Wed", 3) != 0 && - memcmp (StringPntr, "Thu", 3) != 0 && - memcmp (StringPntr, "Fri", 3) != 0 && - memcmp (StringPntr, "Sat", 3) != 0 && - memcmp (StringPntr, "Sun", 3) != 0) - { - fprintf (stderr, "False alarm, not a valid day of the week in \"%s\".\n", - LineString); - return false; - } - StringPntr += 3; - while (*StringPntr == ' ') - StringPntr++; - - /* Look for the 3 letter month code. */ - - if (memcmp (StringPntr, "Jan", 3) != 0 && - memcmp (StringPntr, "Feb", 3) != 0 && - memcmp (StringPntr, "Mar", 3) != 0 && - memcmp (StringPntr, "Apr", 3) != 0 && - memcmp (StringPntr, "May", 3) != 0 && - memcmp (StringPntr, "Jun", 3) != 0 && - memcmp (StringPntr, "Jul", 3) != 0 && - memcmp (StringPntr, "Aug", 3) != 0 && - memcmp (StringPntr, "Sep", 3) != 0 && - memcmp (StringPntr, "Oct", 3) != 0 && - memcmp (StringPntr, "Nov", 3) != 0 && - memcmp (StringPntr, "Dec", 3) != 0) - { - fprintf (stderr, "False alarm, not a valid month name in \"%s\".\n", - LineString); - return false; - } - StringPntr += 3; - while (*StringPntr == ' ') - StringPntr++; - - /* Skip the day of the month. Require at least one digit. */ - - if (*StringPntr < '0' || *StringPntr > '9') - { - fprintf (stderr, "False alarm, not a valid day of the " - "month number in \"%s\".\n", LineString); - return false; - } - while (*StringPntr >= '0' && *StringPntr <= '9') - StringPntr++; - while (*StringPntr == ' ') - StringPntr++; - - /* Check the time. Look for the sequence - digit-digit-colon-digit-digit-colon-digit-digit. */ - - if (StringPntr[0] < '0' || StringPntr[0] > '9' || - StringPntr[1] < '0' || StringPntr[1] > '9' || - StringPntr[2] != ':' || - StringPntr[3] < '0' || StringPntr[3] > '9' || - StringPntr[4] < '0' || StringPntr[4] > '9' || - StringPntr[5] != ':' || - StringPntr[6] < '0' || StringPntr[6] > '9' || - StringPntr[7] < '0' || StringPntr[7] > '9') - { - fprintf (stderr, "False alarm, not a valid time value in \"%s\".\n", - LineString); - return false; - } - StringPntr += 8; - while (*StringPntr == ' ') - StringPntr++; - - /* Look for the optional antique 3 capital letter time zone and skip it. */ - - if (StringPntr[0] >= 'A' && StringPntr[0] <= 'Z' && - StringPntr[1] >= 'A' && StringPntr[1] <= 'Z' && - StringPntr[2] >= 'A' && StringPntr[2] <= 'Z') - { - StringPntr += 3; - while (*StringPntr == ' ') - StringPntr++; - } - - /* Look for the 4 digit year. */ - - if (StringPntr[0] < '0' || StringPntr[0] > '9' || - StringPntr[1] < '0' || StringPntr[1] > '9' || - StringPntr[2] < '0' || StringPntr[2] > '9' || - StringPntr[3] < '0' || StringPntr[3] > '9') - { - fprintf (stderr, "False alarm, not a valid 4 digit year in \"%s\".\n", - LineString); - return false; - } - StringPntr += 4; - while (*StringPntr == ' ') - StringPntr++; - - /* Look for the optional modern time zone and skip over it if present. */ - - if ((StringPntr[0] == '+' || StringPntr[0] == '-') && - StringPntr[1] >= '0' && StringPntr[1] <= '9' && - StringPntr[2] >= '0' && StringPntr[2] <= '9' && - StringPntr[3] >= '0' && StringPntr[3] <= '9' && - StringPntr[4] >= '0' && StringPntr[4] <= '9') - { - StringPntr += 5; - while (*StringPntr == ' ') - StringPntr++; - } - - /* Look for end of string. */ - - if (*StringPntr != 0) - { - fprintf (stderr, "False alarm, extra stuff after the " - "year/time zone in \"%s\".\n", LineString); - return false; - } - - return true; -} - - - -/****************************************************************************** - * Read the input file, convert it to mbox format, and write it to standard - * output. Returns zero if successful, a negative error code if an error - * occured. - */ - -status_t ProcessMessageFile (char *FileName) -{ - char ErrorMessage [B_PATH_NAME_LENGTH + 80]; - int i; - int InputFileDescriptor = -1; - FILE *InputFile = NULL; - int LineNumber; - BString MessageText; - status_t ReturnCode = -1; - char *StringPntr; - char TempString [102400]; - time_t TimeValue; - - fprintf (stderr, "Now processing: \"%s\"\n", FileName); - - /* Open the file. Try to open it in exclusive mode, so that it doesn't - accidentally open the output file, or any other already open file. */ - - InputFileDescriptor = open (FileName, O_RDONLY); - if (InputFileDescriptor < 0) - { - ReturnCode = errno; - DisplayErrorMessage ("Unable to open file", ReturnCode); - return ReturnCode; - } - InputFile = fdopen (InputFileDescriptor, "rb"); - if (InputFile == NULL) - { - ReturnCode = errno; - close (InputFileDescriptor); - DisplayErrorMessage ("fdopen failed to convert file handle to a " - "buffered file", ReturnCode); - return ReturnCode; - } - - /* Extract a text message from the BeMail file. */ - - LineNumber = 0; - while (!feof (InputFile)) - { - /* First read in one line of text. */ - - if (!fgets (TempString, sizeof (TempString), InputFile)) - { - ReturnCode = errno; - if (ferror (InputFile)) - { - sprintf (ErrorMessage, - "Error while reading from \"%s\"", FileName); - DisplayErrorMessage (ErrorMessage, ReturnCode); - goto ErrorExit; - } - break; /* No error, just end of file. */ - } - - /* Remove any trailing control characters (line feed usually, or CRLF). - Might also nuke trailing tabs too. Doesn't usually matter. The main thing - is to allow input files with both LF and CRLF endings (and even CR endings - if you come from the Macintosh world). */ - - StringPntr = TempString + strlen (TempString) - 1; - while (StringPntr >= TempString && *StringPntr < 32) - StringPntr--; - *(++StringPntr) = 0; - - if (LineNumber == 0 && TempString[0] == 0) - continue; /* Skip leading blank lines. */ - LineNumber++; - - /* Prepend the new mbox message header, if the first line of the message - doesn't already have one. */ - - if (LineNumber == 1 && !IsStartOfMailMessage (TempString)) - { - TimeValue = DateStampTime++; - MessageText.Append ("From baron@be.com "); - MessageText.Append (ctime (&TimeValue)); - } - - /* Append the line to the current message text. */ - - MessageText.Append (TempString); - MessageText.Append ("\n"); - } - - /* Remove blank lines from the end of the message (a pet peeve of mine), but - end the message with two new lines to separate it from the next message. */ - - i = MessageText.Length (); - while (i > 0 && (MessageText[i-1] == '\n' || MessageText[i-1] == '\r')) - i--; - MessageText.Truncate (i); - MessageText.Append ("\n\n"); - - /* Write the message out. */ - - if (puts (MessageText.String()) < 0) - { - ReturnCode = errno; - DisplayErrorMessage ("Error while writing the message", ReturnCode); - goto ErrorExit; - } - - ReturnCode = 0; - -ErrorExit: - fclose (InputFile); - return ReturnCode; -} - - - -/****************************************************************************** - * Finally, the main program which drives it all. - */ - -int main (int argc, char** argv) -{ - dirent_t *DirEntPntr; - DIR *DirHandle; - status_t ErrorCode; - char InputPathName [B_PATH_NAME_LENGTH]; - int MessagesDoneCount = 0; - BApplication MyApp ("application/x-vnd.agmsmith.bemailtombox"); - const char *StringPntr; - char TempString [1024 + 256]; - - if (argc <= 1 || argc >= 3) - { - printf ("%s is a utility for converting BeMail e-mail\n", argv[0]); - printf ("files to Unix Pine style e-mail files. It could well\n"); - printf ("work with other Unix style mailbox files. Each message in\n"); - printf ("the input directory is converted and sent to the standard\n"); - printf ("output. Usage:\n\n"); - printf ("bemailtombox InputDirectory >OutputFile\n\n"); - printf ("Public domain, by Alexander G. M. Smith.\n"); - printf ("$Id$\n"); - printf ("$HeadURL$\n"); - return -10; - } - - /* Set the date stamp to the current time. */ - - DateStampTime = time (NULL); - - /* Try to open the input directory. */ - - memset (InputPathName, 0, sizeof (InputPathName)); - strncpy (InputPathName, argv[1], sizeof (InputPathName) - 2); - DirHandle = opendir (InputPathName); - if (DirHandle == NULL) - { - ErrorCode = errno; - sprintf (TempString, "Problems opening directory named \"%s\".", - InputPathName); - DisplayErrorMessage (TempString, ErrorCode); - return ErrorCode; - } - - /* Append a trailing slash to the directory name, if it needs one. */ - - if (InputPathName [strlen (InputPathName) - 1] != '/') - strcat (InputPathName, "/"); - - ErrorCode = 0; - while (ErrorCode == 0 && (DirEntPntr = readdir (DirHandle)) != NULL) - { - for (StringPntr = DirEntPntr->d_name; *StringPntr != 0; StringPntr++) - if (*StringPntr != '.') - break; - if (*StringPntr == 0) - continue; /* Skip all period names, like . or .. or ... etc. */ - - strcpy (TempString, InputPathName); - strcat (TempString, DirEntPntr->d_name); - ErrorCode = ProcessMessageFile (TempString); - MessagesDoneCount++; - } - closedir (DirHandle); - if (ErrorCode != 0) - { - DisplayErrorMessage ("Stopping early because an error occured", ErrorCode); - return ErrorCode; - } - - fprintf (stderr, "Did %d messages successfully.\n", MessagesDoneCount); - return 0; -} diff --git a/src/bin/bemail_utils/mail2mbox.rdef b/src/bin/bemail_utils/mail2mbox.rdef deleted file mode 100644 index 3d88bea8b0..0000000000 --- a/src/bin/bemail_utils/mail2mbox.rdef +++ /dev/null @@ -1,70 +0,0 @@ -/* Resources for the bemailtombox program, mostly for the icon. - $Id$ */ - -resource app_flags B_SINGLE_LAUNCH | B_ARGV_ONLY; - -resource app_signature "application/x-vnd.agmsmith.bemailtombox"; - -resource app_version { - major = 1, - middle = 0, - minor = 0, - variety = B_APPV_FINAL, - internal = 1, - short_info = "BeMailToMbox $Rev$", - long_info = "BeMailToMbox - BeMail to Unix mailbox file converter. $Id$" -}; - -resource large_icon array { - $"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFF60606060606060606060606060606060606060606060FFFFFFFF" - $"FFFFFFFF60606060606060606060606060606060606060606060D2D2ACFFFFFF" - $"FFFFFF60606060606060606060606060606060606060606060D2D2D2D2ACFFFF" - $"FFFF6060606060606060606060606060606060606060606060D2D2D2D2ACFFFF" - $"FFFF8686868686868686868686868686868686868686868686D2D2D2D2ACFFFF" - $"FFFF8686868686868686868686868686868686868686868686D2D2D2D2ACFFFF" - $"FFFF8600868686008600000086868600008686008686860086D2D2D2D2ACFFFF" - $"FFFF8600008600008600868600860086860086008686860086D2D2D2ACACFFFF" - $"FFFF8600860086008600868600860086860086860086008686D2D2D2ACACFFFF" - $"FFFF8600860086008600868600860086860086860086008686D2D2D2ACACFFFF" - $"FFFF8600868686008600000086860086860086860086008686D2D2ACACACFFFF" - $"FFFF8600868686008600868600860086860086868600868686D2D2ACACACFFFF" - $"FFFF8600868686008600868600860086860086868600868686D2D2ACACACFFFF" - $"FFFF8600868686008600868600860086860086860086008686D2D2ACACACFFFF" - $"FFFF8600868686008600868600860086860086860086008686D2D2ACACACFFFF" - $"FFFF8600868686008600868600860086860086860086008686D2D2ACACACFFFF" - $"FFFF8600868686008600868600860086860086008686860086D2ACACACACFFFF" - $"FFFF8600868686008600000086868600008686008686860086D2ACACACACFFFF" - $"FFFF8686868686868686868686868686868686868686868686D2ACACFFFFFFFF" - $"FFFF8686868686868686868686868686868686868686868686FFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" -}; - -resource mini_icon array { - $"FFFFFF6060606060606060606060FFFF" - $"FF6060606060606060606060D2D2ACFF" - $"FF0086008600008600860000D2D2ACFF" - $"FF0000008600008600860000D2D2ACFF" - $"FF0000008600008600860000D2D2ACFF" - $"FF0000008600868600868600D2ACACFF" - $"FF0086008600008600868600D2ACACFF" - $"FF0086008600008600860000D2ACACFF" - $"FF0086008600008600860000D2ACACFF" - $"FF0086008600008600860000D2ACFFFF" - $"FFFFFFFFFFFF1915FFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFF1915FFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFF1915FFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFF1915FFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFF1915FFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFF1915FFFFFFFFFFFFFFFF" -}; diff --git a/src/bin/bemail_utils/mbox2mail.cpp b/src/bin/bemail_utils/mbox2mail.cpp deleted file mode 100644 index 3e8ab925e4..0000000000 --- a/src/bin/bemail_utils/mbox2mail.cpp +++ /dev/null @@ -1,757 +0,0 @@ -/****************************************************************************** - * $Id$ - * - * MboxToBeMail is a utility program that converts Unix mailbox (mbox) files - * (the kind that Pine uses) into e-mail files for use with BeOS. It also - * handles news files from rn and trn, which have messages very similar to mail - * messages but with a different separator line. The input files store - * multiple mail messages in text format separated by "From ..." lines or - * "Article ..." lines. The output is a bunch of separate files, each one with - * one message plus BeOS BFS attributes describing that message. For - * convenience, all the messages that were from one file are put in a specified - * directory. - * - * Command line driven. Version 1.7 and up (which have support for - * international character sets (as requested by "MFC" Zahar from Russia) and - * the Thread attribute) require the libmail.so that comes with the Mail Daemon - * Replacement (MDR) version 2.2.6 or later (http://www.bebits.com/app/2289 or - * http://sourceforge.net/projects/bemaildaemon/). Use version 1.6 (get it - * from the version history pages on BeBits) if you don't want to install MDR. - * - * $Log: MailboxToBeMail.cpp,v $ (manually updated now that SVN is used) - * r13960 | agmsmith | 2005-08-13 22:10:49 -0400 (Sat, 13 Aug 2005) | 3 lines - * More file movement for the BeMail utilities... Content updates later to - * avoid confusing svn. - * - * r13955 | agmsmith | 2005-08-13 19:43:41 -0400 (Sat, 13 Aug 2005) | 5 lines - * Half way through adding some more BeMail related utilities - they use - * libmail.so which isn't backwards compatibile so they need recompiling for - * Haiku - thus better to include them here. Also want spam levels of 1E-6 to - * be visible for genuine messages. - * - * Revision 1.9 2003/12/31 00:37:46 agmsmith - * Use the MDR date parsing routine rather than parsedate, so it - * can handle newer date formats. - * - * Revision 1.8 2003/12/28 19:03:57 agmsmith - * Added an option to save or not save the separator text line. - * - * Revision 1.7 2003/12/28 04:23:21 agmsmith - * Now uses the Mail Daemon Replacement library to parse the headers, - * which means they now handle foreign character sets (converted - * automatically to UTF-8), so Subject and To are correct when they - * are used as part of the file name. - * - * Revision 1.6 2002/01/14 20:23:21 agmsmith - * Include the detection line with the message header, just for completeness. - * - * Revision 1.5 2002/01/14 03:21:43 agmsmith - * Added processing of UseNet articles, and allowed an older date format. - * - * Revision 1.4 2002/01/14 00:06:59 agmsmith - * Don't allow slashes in file names. - * - * Revision 1.3 2002/01/13 23:33:26 agmsmith - * First seemingly working version. - * - * Revision 1.2 2002/01/13 17:57:26 agmsmith - * Can now pick out separate messages from the inbox. - * - * Revision 1.1 2002/01/13 15:25:33 agmsmith - * Initial revision - */ - -/* BeOS headers. */ - -#include -#include -#include -#include - -/* Posix headers. */ - -#include -#include -#include -#include - -/* MDR headers (get the MDR source and add the MDR include/public and -include/numail directories to the project path settings). Or just use -the Haiku build process which is already set up to include the right ones. */ - -#include -#include - - - -/****************************************************************************** - * Globals - */ - -char InputPathName [B_PATH_NAME_LENGTH]; -FILE *InputFile; -BDirectory OutputDir; - - -typedef enum StandardHeaderEnum -{ - STD_HDR_DATE = 0, /* The Date: field. First one since it is numeric. */ - STD_HDR_FROM, /* The whole From: field, including quotes and address. */ - STD_HDR_TO, /* All the stuff in the To: field. */ - STD_HDR_CC, /* All the CC: field (originally means carbon copy). */ - STD_HDR_REPLY, /* Things in the reply-to: field. */ - STD_HDR_SUBJECT, /* The Subject: field. */ - STD_HDR_PRIORITY, /* The Priority: and related fields, usually "Normal". */ - STD_HDR_STATUS, /* The BeOS mail Read / New status text attribute. */ - STD_HDR_THREAD, /* The subject simplified. */ - STD_HDR_NAME, /* The From address simplified into a plain name. */ - STD_HDR_MAX -} StandardHeaderCodes; - -const char *g_StandardAttributeNames [STD_HDR_MAX] = -{ - B_MAIL_ATTR_WHEN, - B_MAIL_ATTR_FROM, - B_MAIL_ATTR_TO, - B_MAIL_ATTR_CC, - B_MAIL_ATTR_REPLY, - B_MAIL_ATTR_SUBJECT, - B_MAIL_ATTR_PRIORITY, - B_MAIL_ATTR_STATUS, - B_MAIL_ATTR_THREAD, - B_MAIL_ATTR_NAME -}; - - - -/****************************************************************************** - * 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". - */ - -static void DisplayErrorMessage ( - const char *MessageString = NULL, - int ErrorNumber = 0, - const char *TitleString = NULL) -{ - char ErrorBuffer [B_PATH_NAME_LENGTH + 80 /* error message */ + 80]; - - if (TitleString == NULL) - TitleString = "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; - } - - fputs (TitleString, stderr); - fputc ('\n', stderr); - fputs (MessageString, stderr); - fputc ('\n', stderr); -} - - - -/****************************************************************************** - * Determine if a line of text is the start of another message. Pine mailbox - * files have messages that start with a line that could say something like - * "From agmsmith@achilles.net Fri Oct 31 21:19:36 EST 1997" or maybe something - * like "From POPmail Mon Oct 20 21:12:36 1997" or in a more modern format, - * "From agmsmith@achilles.net Tue Sep 4 09:04:11 2001 -0400". I generalise it - * to "From blah Day MMM NN XX:XX:XX TZONE1 YYYY TZONE2". Blah is an e-mail - * address you can ignore (just treat it as a word separated by spaces). Day - * is a 3 letter day of the week. MMM is a 3 letter month name. NN is the two - * digit day of the week, has a leading space if the day is less than 10. - * XX:XX:XX is the time, the X's are digits. TZONE1 is the old style optional - * time zone of 3 capital letters. YYYY is the four digit year. TZONE2 is the - * optional modern time zone info, a plus or minus sign and 4 digits. Returns - * true if the line of text (ended with a NUL byte, no line feed or carriage - * returns at the end) is the start of a message. - */ - -bool IsStartOfMailMessage (char *LineString) -{ - char *StringPntr; - - /* It starts with "From " */ - - if (memcmp ("From ", LineString, 5) != 0) - return false; - StringPntr = LineString + 4; - while (*StringPntr == ' ') - StringPntr++; - - /* Skip over the e-mail address (or stop at the end of string). */ - - while (*StringPntr != ' ' && *StringPntr != 0) - StringPntr++; - while (*StringPntr == ' ') - StringPntr++; - - /* Look for the 3 letter day of the week. */ - - if (memcmp (StringPntr, "Mon", 3) != 0 && - memcmp (StringPntr, "Tue", 3) != 0 && - memcmp (StringPntr, "Wed", 3) != 0 && - memcmp (StringPntr, "Thu", 3) != 0 && - memcmp (StringPntr, "Fri", 3) != 0 && - memcmp (StringPntr, "Sat", 3) != 0 && - memcmp (StringPntr, "Sun", 3) != 0) - { - printf ("False alarm, not a valid day of the week in \"%s\".\n", - LineString); - return false; - } - StringPntr += 3; - while (*StringPntr == ' ') - StringPntr++; - - /* Look for the 3 letter month code. */ - - if (memcmp (StringPntr, "Jan", 3) != 0 && - memcmp (StringPntr, "Feb", 3) != 0 && - memcmp (StringPntr, "Mar", 3) != 0 && - memcmp (StringPntr, "Apr", 3) != 0 && - memcmp (StringPntr, "May", 3) != 0 && - memcmp (StringPntr, "Jun", 3) != 0 && - memcmp (StringPntr, "Jul", 3) != 0 && - memcmp (StringPntr, "Aug", 3) != 0 && - memcmp (StringPntr, "Sep", 3) != 0 && - memcmp (StringPntr, "Oct", 3) != 0 && - memcmp (StringPntr, "Nov", 3) != 0 && - memcmp (StringPntr, "Dec", 3) != 0) - { - printf ("False alarm, not a valid month name in \"%s\".\n", - LineString); - return false; - } - StringPntr += 3; - while (*StringPntr == ' ') - StringPntr++; - - /* Skip the day of the month. Require at least one digit. */ - - if (*StringPntr < '0' || *StringPntr > '9') - { - printf ("False alarm, not a valid day of the month number in \"%s\".\n", - LineString); - return false; - } - while (*StringPntr >= '0' && *StringPntr <= '9') - StringPntr++; - while (*StringPntr == ' ') - StringPntr++; - - /* Check the time. Look for the sequence - digit-digit-colon-digit-digit-colon-digit-digit. */ - - if (StringPntr[0] < '0' || StringPntr[0] > '9' || - StringPntr[1] < '0' || StringPntr[1] > '9' || - StringPntr[2] != ':' || - StringPntr[3] < '0' || StringPntr[3] > '9' || - StringPntr[4] < '0' || StringPntr[4] > '9' || - StringPntr[5] != ':' || - StringPntr[6] < '0' || StringPntr[6] > '9' || - StringPntr[7] < '0' || StringPntr[7] > '9') - { - printf ("False alarm, not a valid time value in \"%s\".\n", - LineString); - return false; - } - StringPntr += 8; - while (*StringPntr == ' ') - StringPntr++; - - /* Look for the optional antique 3 capital letter time zone and skip it. */ - - if (StringPntr[0] >= 'A' && StringPntr[0] <= 'Z' && - StringPntr[1] >= 'A' && StringPntr[1] <= 'Z' && - StringPntr[2] >= 'A' && StringPntr[2] <= 'Z') - { - StringPntr += 3; - while (*StringPntr == ' ') - StringPntr++; - } - - /* Look for the 4 digit year. */ - - if (StringPntr[0] < '0' || StringPntr[0] > '9' || - StringPntr[1] < '0' || StringPntr[1] > '9' || - StringPntr[2] < '0' || StringPntr[2] > '9' || - StringPntr[3] < '0' || StringPntr[3] > '9') - { - printf ("False alarm, not a valid 4 digit year in \"%s\".\n", - LineString); - return false; - } - StringPntr += 4; - while (*StringPntr == ' ') - StringPntr++; - - /* Look for the optional modern time zone and skip over it if present. */ - - if ((StringPntr[0] == '+' || StringPntr[0] == '-') && - StringPntr[1] >= '0' && StringPntr[1] <= '9' && - StringPntr[2] >= '0' && StringPntr[2] <= '9' && - StringPntr[3] >= '0' && StringPntr[3] <= '9' && - StringPntr[4] >= '0' && StringPntr[4] <= '9') - { - StringPntr += 5; - while (*StringPntr == ' ') - StringPntr++; - } - - /* Look for end of string. */ - - if (*StringPntr != 0) - { - printf ("False alarm, extra stuff after the year/time zone in \"%s\".\n", - LineString); - return false; - } - - return true; -} - - - -/****************************************************************************** - * Determine if a line of text is the start of a news article. TRN and RN news - * article save files have messages that start with a line that looks like - * "Article 11721 of rec.games.video.3do:". Returns true if the line of text - * (ended with a NUL byte, no line feed or carriage returns at the end) is the - * start of an article. - */ - -bool IsStartOfUsenetArticle (char *LineString) -{ - char *StringPntr; - - /* It starts with "Article " */ - - if (memcmp ("Article ", LineString, 8) != 0) - return false; - StringPntr = LineString + 7; - while (*StringPntr == ' ') - StringPntr++; - - /* Skip the article number. Require at least one digit. */ - - if (*StringPntr < '0' || *StringPntr > '9') - { - printf ("False alarm, not a valid article number in \"%s\".\n", - LineString); - return false; - } - while (*StringPntr >= '0' && *StringPntr <= '9') - StringPntr++; - while (*StringPntr == ' ') - StringPntr++; - - /* Now it should have "of " */ - - if (memcmp ("of ", StringPntr, 3) != 0) - { - printf ("False alarm, article line \"of\" misssing in \"%s\".\n", - LineString); - return false; - } - StringPntr += 2; - while (*StringPntr == ' ') - StringPntr++; - - /* Skip over the newsgroup name (no spaces) to the colon. */ - - while (*StringPntr != ':' && *StringPntr != ' ' && *StringPntr != 0) - StringPntr++; - - if (StringPntr[0] != ':' || StringPntr[1] != 0) - { - printf ("False alarm, article doesn't end with a colon in \"%s\".\n", - LineString); - return false; - } - - return true; -} - - - -/****************************************************************************** - * Saves the message text to a file in the output directory. The file name is - * derived from the message headers. Returns zero if successful, a negative - * error code if an error occured. - */ - -status_t SaveMessage (BString &MessageText) -{ - time_t DateInSeconds; - status_t ErrorCode; - BString FileName; - BString HeaderValues [STD_HDR_MAX]; - int i; - int Length; - BEmailMessage MailMessage; - BFile OutputFile; - char TempString [80]; - struct tm TimeFields; - BString UniqueFileName; - int32 Uniquer; - - /* Remove blank lines from the end of the message (a pet peeve of mine), but - end the message with a single new line to avoid annoying text editors that - like to have it. */ - - i = MessageText.Length (); - while (i > 0 && (MessageText[i-1] == '\n' || MessageText[i-1] == '\r')) - i--; - MessageText.Truncate (i); - MessageText.Append ("\r\n"); - - /* Make a pretend file to hold the message, so the MDR library can use it. */ - - BMemoryIO FakeFile (MessageText.String (), MessageText.Length ()); - - /* Hand the message text off to the MDR library, which will parse it, extract - the subject, sender's name, and other attributes, taking into account the - character set headers. */ - - ErrorCode = MailMessage.SetToRFC822 (&FakeFile, - MessageText.Length (), false /* parse_now - decodes message body */); - if (ErrorCode != B_OK) - { - DisplayErrorMessage ("Mail library was unable to process a mail " - "message for some reason", ErrorCode); - return ErrorCode; - } - - /* Get the values for the standard attributes. NULL if missing. */ - - HeaderValues [STD_HDR_TO] = MailMessage.To (); - HeaderValues [STD_HDR_FROM] = MailMessage.From (); - HeaderValues [STD_HDR_CC] = MailMessage.CC (); - HeaderValues [STD_HDR_DATE] = MailMessage.Date (); - HeaderValues [STD_HDR_REPLY] = MailMessage.ReplyTo (); - HeaderValues [STD_HDR_SUBJECT] = MailMessage.Subject (); - HeaderValues [STD_HDR_STATUS] = "Read"; - if (MailMessage.Priority () != 3 /* Normal */) - { - sprintf (TempString, "%d", MailMessage.Priority ()); - HeaderValues [STD_HDR_PRIORITY] = TempString; - } - - HeaderValues[STD_HDR_THREAD] = HeaderValues[STD_HDR_SUBJECT]; - SubjectToThread (HeaderValues[STD_HDR_THREAD]); - if (HeaderValues[STD_HDR_THREAD].Length() <= 0) - HeaderValues[STD_HDR_THREAD] = "No Subject"; - - HeaderValues[STD_HDR_NAME] = HeaderValues[STD_HDR_FROM]; - extract_address_name (HeaderValues[STD_HDR_NAME]); - - // Generate a file name for the incoming message. - - FileName = HeaderValues [STD_HDR_THREAD]; - if (FileName[0] == '.') - FileName.Prepend ("_"); // Avoid hidden files, starting with a dot. - - // Convert the date into a year-month-day fixed digit width format, so that - // sorting by file name will give all the messages with the same subject in - // order of date. - - DateInSeconds = - ParseDateWithTimeZone (HeaderValues[STD_HDR_DATE].String()); - if (DateInSeconds == -1) - DateInSeconds = 0; /* Set it to the earliest time if date isn't known. */ - - localtime_r (&DateInSeconds, &TimeFields); - sprintf (TempString, "%04d%02d%02d%02d%02d%02d", - TimeFields.tm_year + 1900, - TimeFields.tm_mon + 1, - TimeFields.tm_mday, - TimeFields.tm_hour, - TimeFields.tm_min, - TimeFields.tm_sec); - FileName << " " << TempString << " " << HeaderValues[STD_HDR_NAME]; - FileName.Truncate (240); // reserve space for the uniquer - - // Get rid of annoying characters which are hard to use in the shell. - FileName.ReplaceAll('/','_'); - FileName.ReplaceAll('\'','_'); - FileName.ReplaceAll('"','_'); - FileName.ReplaceAll('!','_'); - FileName.ReplaceAll('<','_'); - FileName.ReplaceAll('>','_'); - while (FileName.FindFirst(" ") >= 0) // Remove multiple spaces. - FileName.Replace(" " /* Old */, " " /* New */, 1024 /* Count */); - - Uniquer = 0; - UniqueFileName = FileName; - while (true) - { - ErrorCode = OutputFile.SetTo (&OutputDir, - const_cast (UniqueFileName.String ()), - B_READ_WRITE | B_CREATE_FILE | B_FAIL_IF_EXISTS); - if (ErrorCode == B_OK) - break; - if (ErrorCode != B_FILE_EXISTS) - { - UniqueFileName.Prepend ("Unable to create file \""); - UniqueFileName.Append ("\" for writing a message to"); - DisplayErrorMessage (UniqueFileName.String (), ErrorCode); - return ErrorCode; - } - Uniquer++; - UniqueFileName = FileName; - UniqueFileName << " " << Uniquer; - } - - /* Write the message contents to the file, use the unchanged original one. */ - - ErrorCode = OutputFile.Write (MessageText.String (), MessageText.Length ()); - if (ErrorCode < 0) - { - UniqueFileName.Prepend ("Error while writing file \""); - UniqueFileName.Append ("\""); - DisplayErrorMessage (UniqueFileName.String (), ErrorCode); - return ErrorCode; - } - - /* Attach the attributes to the file. Save the MIME type first, otherwise - the live queries don't pick up the new file. Theoretically it would be - better to do it last so that other programs don't start reading the message - before the other attributes are set. */ - - OutputFile.WriteAttr ("BEOS:TYPE", B_MIME_STRING_TYPE, 0, - "text/x-email", 13); - - OutputFile.WriteAttr (g_StandardAttributeNames[STD_HDR_DATE], - B_TIME_TYPE, 0, &DateInSeconds, sizeof (DateInSeconds)); - - /* Write out all the string based attributes. */ - - for (i = 1 /* The date was zero */; i < STD_HDR_MAX; i++) - { - if ((Length = HeaderValues[i].Length()) > 0) - OutputFile.WriteAttr (g_StandardAttributeNames[i], B_STRING_TYPE, 0, - HeaderValues[i].String(), Length + 1); - } - - return 0; -} - - - -/****************************************************************************** - * Finally, the main program which drives it all. - */ - -int main (int argc, char** argv) -{ - char ErrorMessage [B_PATH_NAME_LENGTH + 80]; - bool HaveOldMessage = false; - int MessagesDoneCount = 0; - BString MessageText; - BApplication MyApp ("application/x-vnd.agmsmith.mboxtobemail"); - int NextArgIndex; - char OutputDirectoryPathName [B_PATH_NAME_LENGTH]; - status_t ReturnCode = -1; - bool SaveSeparatorLine = false; - char *StringPntr; - char TempString [102400]; - - if (argc <= 1) - { - printf ("%s is a utility for converting Pine e-mail\n", - argv[0]); - printf ("files (mbox files) to BeOS e-mail files with attributes. It\n"); - printf ("could well work with other Unix style mailbox files, and\n"); - printf ("saved Usenet article files. Each message in the input\n"); - printf ("mailbox is converted into a separate file. You can\n"); - printf ("optionally specify a directory (will be created if needed) to\n"); - printf ("put all the output files in, otherwise it scatters them into\n"); - printf ("the current directory. The -s option makes it leave in the\n"); - printf ("separator text line at the top of each message, the default\n"); - printf ("is to lose it.\n\n"); - printf ("Usage:\n\n"); - printf ("mboxtobemail [-s] InputFile [OutputDirectory]\n\n"); - printf ("Public domain, by Alexander G. M. Smith.\n"); - printf ("$Id$\n"); - printf ("$HeadURL$\n"); - return -10; - } - - NextArgIndex = 1; - if (strcmp (argv[NextArgIndex], "-s") == 0) - { - SaveSeparatorLine = true; - NextArgIndex++; - } - - /* Try to open the input file. */ - - if (NextArgIndex >= argc) - { - ReturnCode = -20; - DisplayErrorMessage ("Missing the input file (mbox file) name argument."); - goto ErrorExit; - } - strncpy (InputPathName, argv[NextArgIndex], sizeof (InputPathName) - 1); - NextArgIndex++; - InputFile = fopen (InputPathName, "rb"); - if (InputFile == NULL) - { - ReturnCode = errno; - sprintf (ErrorMessage, "Unable to open file \"%s\" for reading", - InputPathName); - DisplayErrorMessage (ErrorMessage, ReturnCode); - goto ErrorExit; - } - - /* Try to make the output directory. First get its name. */ - - if (NextArgIndex < argc) - { - strncpy (OutputDirectoryPathName, argv[NextArgIndex], - sizeof (OutputDirectoryPathName) - 2 - /* Leave space for adding trailing slash and NUL byte */); - NextArgIndex++; - } - else - strcpy (OutputDirectoryPathName, "."); - - /* Remove trailing '/' characters from the output directory path. */ - - StringPntr = - OutputDirectoryPathName + (strlen (OutputDirectoryPathName) - 1); - while (StringPntr >= OutputDirectoryPathName) - { - if (*StringPntr != '/') - break; - StringPntr--; - } - *(++StringPntr) = 0; - - if (StringPntr - OutputDirectoryPathName > 0 && - strcmp (OutputDirectoryPathName, ".") != 0) - { - if (mkdir (OutputDirectoryPathName, 0777)) - { - ReturnCode = errno; - if (ReturnCode != B_FILE_EXISTS) - { - sprintf (ErrorMessage, "Unable to make output directory \"%s\"", - OutputDirectoryPathName); - DisplayErrorMessage (ErrorMessage, ReturnCode); - goto ErrorExit; - } - } - } - - /* Set the output BDirectory. */ - - ReturnCode = OutputDir.SetTo (OutputDirectoryPathName); - if (ReturnCode != B_OK) - { - sprintf (ErrorMessage, "Unable to set output BDirectory to \"%s\"", - OutputDirectoryPathName); - DisplayErrorMessage (ErrorMessage, ReturnCode); - goto ErrorExit; - } - - printf ("Input file: \"%s\", Output directory: \"%s\", ", - InputPathName, OutputDirectoryPathName); - printf ("%ssaving separator text line at the top of each message. Working", - SaveSeparatorLine ? "" : "not "); - - /* Extract a text message from the mail file. It starts with a line that - says "From blah Day MM NN XX:XX:XX YYYY TZONE". Blah is an e-mail address - you can ignore (just treat it as a word separated by spaces). Day is a 3 - letter day of the week. MM is a 3 letter month name. NN is the two digit - day of the week, has a leading space if the day is less than 10. XX:XX:XX is - the time, the X's are digits. YYYY is the four digit year. TZONE is the - optional time zone info, a plus or minus sign and 4 digits. */ - - while (!feof (InputFile)) - { - /* First read in one line of text. */ - - if (!fgets (TempString, sizeof (TempString), InputFile)) - { - ReturnCode = errno; - if (ferror (InputFile)) - { - sprintf (ErrorMessage, - "Error while reading from \"%s\"", InputPathName); - DisplayErrorMessage (ErrorMessage, ReturnCode); - goto ErrorExit; - } - break; /* No error, just end of file. */ - } - - /* Remove any trailing control characters (line feed usually, or CRLF). - Might also nuke trailing tabs too. Doesn't usually matter. The main thing - is to allow input files with both LF and CRLF endings (and even CR endings - if you come from the Macintosh world). */ - - StringPntr = TempString + strlen (TempString) - 1; - while (StringPntr >= TempString && *StringPntr < 32) - StringPntr--; - *(++StringPntr) = 0; - - /* See if this is the start of a new message. */ - - if (IsStartOfUsenetArticle (TempString) || - IsStartOfMailMessage (TempString)) - { - if (HaveOldMessage) - { - if ((ReturnCode = SaveMessage (MessageText)) != 0) - goto ErrorExit; - putchar ('.'); - fflush (stdout); - MessagesDoneCount++; - } - HaveOldMessage = true; - MessageText.SetTo (SaveSeparatorLine ? TempString : ""); - } - else - { - /* Append the line to the current message text. */ - - if (MessageText.Length () > 0) - MessageText.Append ("\r\n"); /* Yes, BeMail expects CR/LF line ends. */ - MessageText.Append (TempString); - } - } - - /* Flush out the last message. */ - - if (HaveOldMessage) - { - if ((ReturnCode = SaveMessage (MessageText)) != 0) - goto ErrorExit; - putchar ('.'); - MessagesDoneCount++; - } - printf (" Did %d messages.\n", MessagesDoneCount); - - ReturnCode = 0; - -ErrorExit: - if (InputFile != NULL) - fclose (InputFile); - OutputDir.Unset (); - return ReturnCode; -} diff --git a/src/bin/bemail_utils/mbox2mail.rdef b/src/bin/bemail_utils/mbox2mail.rdef deleted file mode 100644 index 1854859c07..0000000000 --- a/src/bin/bemail_utils/mbox2mail.rdef +++ /dev/null @@ -1,71 +0,0 @@ -/* Resources for the mboxtobemail program, mostly for the icon. - $Id$ */ - -resource app_flags B_SINGLE_LAUNCH | B_ARGV_ONLY; - -resource app_signature "application/x-vnd.agmsmith.mboxtobemail"; - -resource app_version { - major = 1, - middle = 0, - minor = 0, - variety = B_APPV_FINAL, - internal = 1, - short_info = "MboxToBeMail $Rev$", - long_info = "MboxToBeMail - Unix mailbox to BeMail file converter. $Id$" -}; - - -resource large_icon { - $"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFF60606060606060606060606060606060606060606060FFFFFFFF" - $"FFFFFFFF60606060606060606060606060606060606060606060D2D2ACFFFFFF" - $"FFFFFF60606060606060606060606060606060606060606060D2D2D2D2ACFFFF" - $"FFFF6060606060606060606060606060606060606060606060D2D2D2D2ACFFFF" - $"FFFF8686868686868686868686868686868686868686868686D2D2D2D2ACFFFF" - $"FFFF8686868686868686868686868686868686868686868686D2D2D2D2ACFFFF" - $"FFFF8686262626268686262626862686868626862626262686D2D2D2D2ACFFFF" - $"FFFF8686268686862686862686862686868626862686868686D2D2D2ACACFFFF" - $"FFFF8686268686862686862686862626868626862686868686D2D2D2ACACFFFF" - $"FFFF8686268686862686862686862626868626862686868686D2D2D2ACACFFFF" - $"FFFF8686262626268686862686862626868626862686868686D2D2ACACACFFFF" - $"FFFF8686268686868686862686862686268626862626868686D2D2ACACACFFFF" - $"FFFF8686268686868686862686862686268626862686868686D2D2ACACACFFFF" - $"FFFF8686268686868686862686862686268626862686868686D2D2ACACACFFFF" - $"FFFF8686268686868686862686862686862626862686868686D2D2ACACACFFFF" - $"FFFF8686268686868686862686862686862626862686868686D2D2ACACACFFFF" - $"FFFF8686268686868686862686862686868626862686868686D2ACACACACFFFF" - $"FFFF8686268686868686262626862686868626862626262686D2ACACACACFFFF" - $"FFFF8686868686868686868686868686868686868686868686D2ACACFFFFFFFF" - $"FFFF8686868686868686868686868686868686868686868686FFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFFFFFF1519191515FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" -}; - -resource mini_icon { - $"FFFFFF6060606060606060606060FFFF" - $"FF6060606060606060606060D2D2ACFF" - $"FF8686868686868686868686D2D2ACFF" - $"FF862626862686268626862626D2ACFF" - $"FF8626268626862686268626D2D2ACFF" - $"FF862626862686262626862626ACACFF" - $"FF8626868626862626268626D2ACACFF" - $"FF8626868626862686268626D2ACACFF" - $"FF862686862686268626862626ACACFF" - $"FF8686868686868686868686D2ACFFFF" - $"FFFFFFFFFFFF1915FFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFF1915FFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFF1915FFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFF1915FFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFF1915FFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFF1915FFFFFFFFFFFFFFFF" -}; diff --git a/src/bin/bemail_utils/spamdbm.cpp b/src/bin/bemail_utils/spamdbm.cpp deleted file mode 100644 index 88c3eb4fcc..0000000000 --- a/src/bin/bemail_utils/spamdbm.cpp +++ /dev/null @@ -1,7797 +0,0 @@ -/****************************************************************************** - * $Id$ - * - * 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 vacation 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. - * - * Revision History (now manually updated due to SVN's philosophy) - * $Log: spamdbm.cpp,v $ - * ------------------------------------------------------------------------ - * r15195 | agmsmith | 2005-11-27 21:07:55 -0500 (Sun, 27 Nov 2005) | 4 lines - * Just a few minutes after checking in, I mentioned it to Japanese expert Koki - * and he suggested also including the Japanese comma. So before I forget to - * do it... - * - * ------------------------------------------------------------------------ - * r15194 | agmsmith | 2005-11-27 20:37:13 -0500 (Sun, 27 Nov 2005) | 5 lines - * Truncate overly long URLs to the maximum word length. Convert Japanese - * periods to spaces so that more "words" are found. Fix UTF-8 comparison - * problems with tolower() incorrectly converting characters with the high bit - * set. - * - * r15098 | agmsmith | 2005-11-23 23:17:00 -0500 (Wed, 23 Nov 2005) | 5 lines - * Added better tokenization so that HTML is parsed and things like tags - * between letters of a word no longer hide that word. After testing, the - * result seems to be a tighter spread of ratings when done in full text plus - * header mode. - * - * Revision 1.10 2005/11/24 02:08:39 agmsmith - * Fixed up prefix codes, Z for things that are inside other things. - * - * Revision 1.9 2005/11/21 03:28:03 agmsmith - * Added a function for extracting URLs. - * - * Revision 1.8 2005/11/09 03:36:18 agmsmith - * Removed noframes detection (doesn't show up in e-mails). Now use - * just H for headers and Z for HTML tag junk. - * - * Revision 1.7 2005/10/24 00:00:08 agmsmith - * Adding HTML tag removal, which also affected the search function so it - * could search for single part things like  . - * - * Revision 1.6 2005/10/17 01:55:08 agmsmith - * Remove HTML comments and a few other similar things. - * - * Revision 1.5 2005/10/16 18:35:36 agmsmith - * Under construction - looking into HTML not being in UTF-8. - * - * Revision 1.4 2005/10/11 01:51:21 agmsmith - * Starting on the tokenising passes. Still need to test asian truncation. - * - * Revision 1.3 2005/10/06 11:54:07 agmsmith - * Not much. - * - * Revision 1.2 2005/09/12 01:49:37 agmsmith - * Enable case folding for the whole file tokenizer. - * - * r13961 | agmsmith | 2005-08-13 22:25:28 -0400 (Sat, 13 Aug 2005) | 2 lines - * Source code changes so that mboxtobemail now compiles and is in the build - * system. - * - * r13959 | agmsmith | 2005-08-13 22:05:27 -0400 (Sat, 13 Aug 2005) | 2 lines - * Rename the directory before doing anything else, otherwise svn dies badly. - * - * r13952 | agmsmith | 2005-08-13 15:31:42 -0400 (Sat, 13 Aug 2005) | 3 lines - * Added the resources and file type associations, changed the application - * signature and otherwise made the spam detection system work properly again. - * - * r13951 | agmsmith | 2005-08-13 11:40:01 -0400 (Sat, 13 Aug 2005) | 2 lines - * Had to do the file rename as a separate operation due to SVN limitations. - * - * r13950 | agmsmith | 2005-08-13 11:38:44 -0400 (Sat, 13 Aug 2005) | 3 lines - * Oops, "spamdb" is already used for a Unix package. And spamdatabase is - * already reserved by a domain name squatter. Use "spamdbm" instead. - * - * r13949 | agmsmith | 2005-08-13 11:17:52 -0400 (Sat, 13 Aug 2005) | 3 lines - * Renamed spamfilter to be the more meaningful spamdb (spam database) and - * moved it into its own source directory in preparation for adding resources. - * - * r13628 | agmsmith | 2005-07-10 20:11:29 -0400 (Sun, 10 Jul 2005) | 3 lines - * Updated keyword expansion to use SVN keywords. Also seeing if svn is - * working well enough for me to update files from BeOS R5. - * - * r11909 | axeld | 2005-03-18 19:09:19 -0500 (Fri, 18 Mar 2005) | 2 lines - * Moved bin/ directory out of apps/. - * - * r11769 | bonefish | 2005-03-17 03:30:54 -0500 (Thu, 17 Mar 2005) | 1 line - * Move trunk into respective module. - * - * r10362 | nwhitehorn | 2004-12-06 20:14:05 -0500 (Mon, 06 Dec 2004) | 2 lines - * Fixed the spam filter so it works correctly now. - * - * r9934 | nwhitehorn | 2004-11-11 21:55:05 -0500 (Thu, 11 Nov 2004) | 2 lines - * Added AGMS's excellent spam detection software. Still some weirdness with - * the configuration interface from E-mail prefs. - * - * Revision 1.2 2004/12/07 01:14:05 nwhitehorn - * Fixed the spam filter so it works correctly now. - * - * 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 - -/* Standard C++ library. */ - -#include - -/* STL (Standard Template Library) headers. */ - -#include -#include -#include -#include -#include - -using namespace std; - -/* 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 -#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.spamdbm"; - -static const char *g_ABSDatabaseFileMIMEType = - "text/x-vnd.agmsmith.spam_probability_database"; - -static const char *g_DefaultDatabaseFileName = - "SpamDBM 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 = "SpamFilter-Genuine"; -static const char *g_BeepSpam = "SpamFilter-Spam"; -static const char *g_BeepUncertain = "SpamFilter-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 = "Mail"; -static const char *g_SettingsFileName = "SpamDBM Settings"; -static const uint32 g_SettingsWhatCode = 'SDBM'; -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. */ - -static BCursor *g_BusyCursor = NULL; - /* The busy cursor, will be loaded from the resource file during application - startup. */ - -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 / " - "Mail / SpamDBM 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] = -{ - "All", - "Plain Text", - "Plain Text and Header", - "Any Text", - "Any Text and Header", - "All Parts", - "All Parts and Header", - "Just Header" -}; - - -/* 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)) -}; - - -/* An array of flags to identify characters which are considered to be spaces. -If character code X has g_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. Initialised in the ABSApp constructor. */ - -static bool g_SpaceCharacters [128]; - - - -/****************************************************************************** - * 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; -}; - - -/* 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'; - -/****************************************************************************** - * 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: - /* 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, - char PrefixCharacter, 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, int MaxRecursionLevel); - 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. */ - - 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 = "SpamDBM 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, const 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 << "\nSpamDBM - A Spam Database Manager\n"; - OutputStream << "Copyright © 2002 by Alexander G. M. Smith. "; - OutputStream << "Released to the public domain.\n\n"; - WrapTextToStream (OutputStream, "Compiled on " __DATE__ " at " __TIME__ -". $Id$ $HeadURL$"); - 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" -"Tokenising code updated in 2005 to use some of the tricks that SpamBayes\n" -"uses to extract words from messages. In particular, HTML is now handled.\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, (char *)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; -} - - - -/****************************************************************************** - * The tokenizing functions. To make tokenization of the text easier to - * understand, it is broken up into several passes. Each pass goes over the - * text (can include NUL bytes) and extracts all the words it can recognise - * (can be none). The extracted words are added to the WordSet, with the - * PrefixCharacter prepended (zero if none) so we can distinguish between words - * found in headers and in the text body. It also modifies the input text - * buffer in-place to change the text that the next pass will see (blanking out - * words that it wants to delete, but not inserting much new text since the - * buffer can't be enlarged). They all return the number of bytes remaining in - * InputString after it has been modified to be input for the next pass. - * Returns zero if it has exhausted the possibility of getting more words, or - * if something goes wrong. - */ - -static size_t TokenizerPassLowerCase ( - char *BufferPntr, - size_t NumberOfBytes) -{ - char *EndOfStringPntr; - - EndOfStringPntr = BufferPntr + NumberOfBytes; - - while (BufferPntr < EndOfStringPntr) - { - /* Do our own lower case conversion; tolower () has problems with UTF-8 - characters that have the high bit set. */ - - if (*BufferPntr >= 'A' && *BufferPntr <= 'Z') - *BufferPntr = *BufferPntr + ('a' - 'A'); - BufferPntr++; - } - return NumberOfBytes; -} - - -/* A utility function for some commonly repeated code. If this was Modula-2, -we could use a nested procedure. But it's not. Adds the given word to the set -of words, checking for maximum word length and prepending the prefix to the -word, which gets modified by this function to reflect the word actually added -to the set. */ - -static void AddWordAndPrefixToSet ( - string &Word, - const char *PrefixString, - set &WordSet) -{ - if (Word.empty ()) - return; - - if (Word.size () > g_MaxWordLength) - Word.resize (g_MaxWordLength); - Word.insert (0, PrefixString); - WordSet.insert (Word); -} - - -/* Hunt through the text for various URLs and extract the components as -separate words. Doesn't affect the text in the buffer. Looks for -protocol://user:password@computer:port/path?query=key#anchor strings. Also -www.blah strings are detected and broken down. Doesn't do HREF="" strings -where the string has a relative path (no host computer name). Assumes the -input buffer is already in lower case. */ - -static size_t TokenizerPassExtractURLs ( - char *BufferPntr, - size_t NumberOfBytes, - char PrefixCharacter, - set &WordSet) -{ - char *AtSignStringPntr; - char *HostStringPntr; - char *InputStringEndPntr; - char *InputStringPntr; - char *OptionsStringPntr; - char *PathStringPntr; - char PrefixString [2]; - char *ProtocolStringPntr; - string Word; - - InputStringPntr = BufferPntr; - InputStringEndPntr = BufferPntr + NumberOfBytes; - PrefixString [0] = PrefixCharacter; - PrefixString [1] = 0; - - while (InputStringPntr < InputStringEndPntr - 4) - { - HostStringPntr = NULL; - if (memcmp (InputStringPntr, "www.", 4) == 0) - HostStringPntr = InputStringPntr; - else if (memcmp (InputStringPntr, "://", 3) == 0) - { - /* Find the protocol name, and add it as a word such as "ftp:" "http:" */ - ProtocolStringPntr = InputStringPntr; - while (ProtocolStringPntr > BufferPntr && - isalpha (ProtocolStringPntr[-1])) - ProtocolStringPntr--; - Word.assign (ProtocolStringPntr, - (InputStringPntr - ProtocolStringPntr) + 1 /* for the colon */); - AddWordAndPrefixToSet (Word, PrefixString, WordSet); - HostStringPntr = InputStringPntr + 3; /* Skip past the "://" */ - } - if (HostStringPntr == NULL) - { - InputStringPntr++; - continue; - } - - /* Got a host name string starting at HostStringPntr. It's everything - until the next slash or space, like "user:password@computer:port". */ - - InputStringPntr = HostStringPntr; - AtSignStringPntr = NULL; - while (InputStringPntr < InputStringEndPntr && - (*InputStringPntr != '/' && !isspace (*InputStringPntr))) - { - if (*InputStringPntr == '@') - AtSignStringPntr = InputStringPntr; - InputStringPntr++; - } - if (AtSignStringPntr != NULL) - { - /* Add a word with the user and password, unseparated. */ - Word.assign (HostStringPntr, - AtSignStringPntr - HostStringPntr + 1 /* for the @ sign */); - AddWordAndPrefixToSet (Word, PrefixString, WordSet); - HostStringPntr = AtSignStringPntr + 1; - } - - /* Add a word with the computer and port, unseparated. */ - - Word.assign (HostStringPntr, InputStringPntr - HostStringPntr); - AddWordAndPrefixToSet (Word, PrefixString, WordSet); - - /* Now get the path name, not including the extra junk after ? and # - separators (they're stored as separate options). Stops at white space or a - double quote mark. */ - - PathStringPntr = InputStringPntr; - OptionsStringPntr = NULL; - while (InputStringPntr < InputStringEndPntr && - (*InputStringPntr != '"' && !isspace (*InputStringPntr))) - { - if (OptionsStringPntr == NULL && - (*InputStringPntr == '?' || *InputStringPntr == '#')) - OptionsStringPntr = InputStringPntr; - InputStringPntr++; - } - - if (OptionsStringPntr == NULL) - { - /* No options, all path. */ - Word.assign (PathStringPntr, InputStringPntr - PathStringPntr); - AddWordAndPrefixToSet (Word, PrefixString, WordSet); - } - else - { - /* Insert the path before the options. */ - Word.assign (PathStringPntr, OptionsStringPntr - PathStringPntr); - AddWordAndPrefixToSet (Word, PrefixString, WordSet); - - /* Insert all the options as a word. */ - Word.assign (OptionsStringPntr, InputStringPntr - OptionsStringPntr); - AddWordAndPrefixToSet (Word, PrefixString, WordSet); - } - } - return NumberOfBytes; -} - - -/* Replace long Asian words (likely to actually be sentences) with the first -character in the word. */ - -static size_t TokenizerPassTruncateLongAsianWords ( - char *BufferPntr, - size_t NumberOfBytes) -{ - char *EndOfStringPntr; - char *InputStringPntr; - int Letter; - char *OutputStringPntr; - char *StartOfInputLongUnicodeWord; - char *StartOfOutputLongUnicodeWord; - - InputStringPntr = BufferPntr; - EndOfStringPntr = InputStringPntr + NumberOfBytes; - OutputStringPntr = InputStringPntr; - StartOfInputLongUnicodeWord = NULL; /* Non-NULL flags it as started. */ - StartOfOutputLongUnicodeWord = NULL; - - /* Copy the text from the input to the output (same buffer), but when we find - a sequence of UTF-8 characters that is too long then truncate it down to one - character and reset the output pointer to be after that character, thus - deleting the word. Replacing the deleted characters after it with spaces - won't work since we need to preserve the lack of space to handle those sneaky - HTML artificial word breakers. So that Thelongwording becomes - "Ting" rather than "T ing", so the next step joins them up into - "Ting" rather than "T" and "ing". The first code in a UTF-8 character is - 11xxxxxx and subsequent ones are 10xxxxxx. */ - - while (InputStringPntr < EndOfStringPntr) - { - Letter = (unsigned char) *InputStringPntr; - if (Letter < 128) // Got a regular ASCII letter? - { - if (StartOfInputLongUnicodeWord != NULL) - { - if (InputStringPntr - StartOfInputLongUnicodeWord > - (int) g_MaxWordLength * 2) - { - /* Need to truncate the long word (100 bytes or about 50 characters) - back down to the first UTF-8 character, so find out where the first - character ends (skip past the 10xxxxxx bytes), and rewind the output - pointer to be just after that (ignoring the rest of the long word in - effect). */ - - OutputStringPntr = StartOfOutputLongUnicodeWord + 1; - while (OutputStringPntr < InputStringPntr) - { - Letter = (unsigned char) *OutputStringPntr; - if (Letter < 128 || Letter >= 192) - break; - ++OutputStringPntr; // Still a UTF-8 middle of the character code. - } - } - StartOfInputLongUnicodeWord = NULL; - } - } - else if (Letter >= 192 && StartOfInputLongUnicodeWord == NULL) - { - /* Got the start of a UTF-8 character. Remember the spot so we can see - if this is a too long UTF-8 word, which is often a whole sentence in - asian languages, since they sort of use a single character per word. */ - - StartOfInputLongUnicodeWord = InputStringPntr; - StartOfOutputLongUnicodeWord = OutputStringPntr; - } - *OutputStringPntr++ = *InputStringPntr++; - } - return OutputStringPntr - BufferPntr; -} - - -/* Find all the words in the string and add them to our local set of words. -The characters considered white space are defined by g_SpaceCharacters. This -function is also used as a subroutine by other tokenizer functions when they -have a bunch of presumably plain text they want broken into words and added. */ - -static size_t TokenizerPassGetPlainWords ( - char *BufferPntr, - size_t NumberOfBytes, - char PrefixCharacter, - set &WordSet) -{ - string AccumulatedWord; - char *EndOfStringPntr; - size_t Length; - int Letter; - - if (NumberOfBytes <= 0) - return 0; /* Nothing to process. */ - - if (PrefixCharacter != 0) - AccumulatedWord = PrefixCharacter; - EndOfStringPntr = BufferPntr + NumberOfBytes; - while (true) - { - if (BufferPntr >= EndOfStringPntr) - Letter = EOF; // Usually a negative number. - else - Letter = (unsigned char) *BufferPntr++; - - /* See if it is a letter we treat as white space. Some word separators - like dashes and periods aren't considered as space. Note that codes above - 127 are UTF-8 characters, which we consider non-space. */ - - if (Letter < 0 /* EOF is -1 */ || - (Letter < 128 && g_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 (PrefixCharacter != 0) - Length--; // Don't count prefix when judging size or emptiness. - if (Length > 0 && Length <= g_MaxWordLength) - WordSet.insert (AccumulatedWord); - - /* Empty out the string to get ready for the next word. Not quite empty, - start it off with the prefix character if any. */ - - if (PrefixCharacter != 0) - AccumulatedWord = PrefixCharacter; - else - 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. */ - } - return NumberOfBytes; -} - - -/* Delete Things from the text. The Thing is marked by a start string and an -end string, such as " for HTML comment things. All the text -between the markers will be added to the word list before it gets deleted from -the buffer. The markers must be prepared in lower case and the buffer is -assumed to have already been converted to lower case. You can specify an empty -string for the end marker if you're just matching a string constant like -" ", which you would put in the starting marker. This is a utility -function used by other tokenizer functions. */ - -static size_t TokenizerUtilRemoveStartEndThing ( - char *BufferPntr, - size_t NumberOfBytes, - char PrefixCharacter, - set &WordSet, - const char *ThingStartCode, - const char *ThingEndCode, - bool ReplaceWithSpace) -{ - char *EndOfStringPntr; - bool FoundAndDeletedThing; - char *InputStringPntr; - char *OutputStringPntr; - int ThingEndLength; - char *ThingEndPntr; - int ThingStartLength; - - InputStringPntr = BufferPntr; - EndOfStringPntr = InputStringPntr + NumberOfBytes; - OutputStringPntr = InputStringPntr; - ThingStartLength = strlen (ThingStartCode); - ThingEndLength = strlen (ThingEndCode); - - if (ThingStartLength <= 0) - return NumberOfBytes; /* Need some things to look for first! */ - - while (InputStringPntr < EndOfStringPntr) - { - /* Search for the starting marker. */ - - FoundAndDeletedThing = false; - if (EndOfStringPntr - InputStringPntr >= - ThingStartLength + ThingEndLength /* space remains for start + end */ && - *InputStringPntr == *ThingStartCode && - memcmp (InputStringPntr, ThingStartCode, ThingStartLength) == 0) - { - /* Found the start marker. Look for the terminating string. If it is an - empty string, then we've found it right now! */ - - ThingEndPntr = InputStringPntr + ThingStartLength; - while (EndOfStringPntr - ThingEndPntr >= ThingEndLength) - { - if (ThingEndLength == 0 || - (*ThingEndPntr == *ThingEndCode && - memcmp (ThingEndPntr, ThingEndCode, ThingEndLength) == 0)) - { - /* Got the end of the Thing. First dump the text inbetween the start - and end markers into the words list. */ - - TokenizerPassGetPlainWords (InputStringPntr + ThingStartLength, - ThingEndPntr - (InputStringPntr + ThingStartLength), - PrefixCharacter, WordSet); - - /* Delete by not updating the output pointer while moving the input - pointer to just after the ending tag. */ - - InputStringPntr = ThingEndPntr + ThingEndLength; - if (ReplaceWithSpace) - *OutputStringPntr++ = ' '; - FoundAndDeletedThing = true; - break; - } - ThingEndPntr++; - } /* End while ThingEndPntr */ - } - if (!FoundAndDeletedThing) - *OutputStringPntr++ = *InputStringPntr++; - } /* End while InputStringPntr */ - - return OutputStringPntr - BufferPntr; -} - - -static size_t TokenizerPassRemoveHTMLComments ( - char *BufferPntr, - size_t NumberOfBytes, - char PrefixCharacter, - set &WordSet) -{ - return TokenizerUtilRemoveStartEndThing (BufferPntr, NumberOfBytes, - PrefixCharacter, WordSet, "", false); -} - - -static size_t TokenizerPassRemoveHTMLStyle ( - char *BufferPntr, - size_t NumberOfBytes, - char PrefixCharacter, - set &WordSet) -{ - return TokenizerUtilRemoveStartEndThing (BufferPntr, NumberOfBytes, - PrefixCharacter, WordSet, - "", false /* replace with space if true */); -} - - -/* Convert Japanese periods (a round hollow dot symbol) to spaces so that the -start of the next sentence is recognised at least as the start of a very long -word. The Japanese comma also does the same job. */ - -static size_t TokenizerPassJapanesePeriodsToSpaces ( - char *BufferPntr, - size_t NumberOfBytes, - char PrefixCharacter, - set &WordSet) -{ - size_t BytesRemaining = NumberOfBytes; - - BytesRemaining = TokenizerUtilRemoveStartEndThing (BufferPntr, - BytesRemaining, PrefixCharacter, WordSet, "。" /* period */, "", true); - BytesRemaining = TokenizerUtilRemoveStartEndThing (BufferPntr, - BytesRemaining, PrefixCharacter, WordSet, "、" /* comma */, "", true); - return BytesRemaining; -} - - -/* Delete HTML tags from the text. The contents of the tag are added as words -before being deleted.

,
and   are replaced by spaces at this -stage while other HTML things get replaced by nothing. */ - -static size_t TokenizerPassRemoveHTMLTags ( - char *BufferPntr, - size_t NumberOfBytes, - char PrefixCharacter, - set &WordSet) -{ - size_t BytesRemaining = NumberOfBytes; - - BytesRemaining = TokenizerUtilRemoveStartEndThing (BufferPntr, - BytesRemaining, PrefixCharacter, WordSet, " ", "", true); - BytesRemaining = TokenizerUtilRemoveStartEndThing (BufferPntr, - BytesRemaining, PrefixCharacter, WordSet, "", true); - BytesRemaining = TokenizerUtilRemoveStartEndThing (BufferPntr, - BytesRemaining, PrefixCharacter, WordSet, "", true); - BytesRemaining = TokenizerUtilRemoveStartEndThing (BufferPntr, - BytesRemaining, PrefixCharacter, WordSet, "<", ">", false); - return BytesRemaining; -} - - - -/****************************************************************************** - * 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; - const void *ResourceData; - size_t ResourceSize; - BResources *ResourcesPntr; - - 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 (g_SpaceCharacters, 1, sizeof (g_SpaceCharacters)); - g_SpaceCharacters['\''] = false; - g_SpaceCharacters['-'] = false; - g_SpaceCharacters['$'] = false; - g_SpaceCharacters['.'] = false; - for (i = '0'; i <= '9'; i++) - g_SpaceCharacters[i] = false; - for (i = 'A'; i <= 'Z'; i++) - g_SpaceCharacters[i] = false; - for (i = 'a'; i <= 'z'; i++) - g_SpaceCharacters[i] = false; - - /* Initialise the busy cursor from data in the application's resources. */ - - if ((ResourcesPntr = AppResources ()) != NULL && (ResourceData = - ResourcesPntr->LoadResource ('CURS', "Busy Cursor", &ResourceSize)) != NULL - && ResourceSize >= 68 /* Size of a raw 2x16x16x8+4 cursor is 68 bytes */) - g_BusyCursor = new BCursor (ResourceData); - - /* 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"); - delete g_BusyCursor; - g_BusyCursor = NULL; -} - - -/* Display a box showing information about this program. */ - -void ABSApp::AboutRequested () -{ - BAlert *AboutAlertPntr; - - AboutAlertPntr = new BAlert ("About", -"SpamDBM - Spam Database Manager\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" - -"Tokenising code updated in 2005 to use some of the tricks that SpamBayes \ -uses to extract words from messages. In particular, HTML is now handled.\n\n" - -"Released to the public domain, with no warranty.\n" -"$Revision$\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 B_NO_MEMORY; - } - - /* 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 B_NO_MEMORY; - } - 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, find the words within it (doing special tricks to -extract words from HTML), and add them to the set. Allow NULs in the text. If -the PrefixCharacter isn't zero then it is prepended to all words found (so you -can distinguish words as being from a header or from the body text). See also -TokenizeWhole which does something similar. */ - -void ABSApp::AddWordsToSet ( - const char *InputString, - size_t NumberOfBytes, - char PrefixCharacter, - set &WordSet) -{ - char *BufferPntr; - size_t CurrentSize; - int PassNumber; - - /* Copy the input buffer. The code will be modifying it in-place as HTML - fragments and other junk are deleted. */ - - BufferPntr = new char [NumberOfBytes]; - if (BufferPntr == NULL) - return; - memcpy (BufferPntr, InputString, NumberOfBytes); - - /* Do the tokenization. Each pass does something to the text in the buffer, - and may add words to the word set. */ - - CurrentSize = NumberOfBytes; - for (PassNumber = 1; PassNumber <= 8 && CurrentSize > 0 ; PassNumber++) - { - switch (PassNumber) - { - case 1: /* Lowercase first, rest of them assume lower case inputs. */ - CurrentSize = TokenizerPassLowerCase (BufferPntr, CurrentSize); - break; - case 2: CurrentSize = TokenizerPassJapanesePeriodsToSpaces ( - BufferPntr, CurrentSize, PrefixCharacter, WordSet); break; - case 3: CurrentSize = TokenizerPassTruncateLongAsianWords ( - BufferPntr, CurrentSize); break; - case 4: CurrentSize = TokenizerPassRemoveHTMLComments ( - BufferPntr, CurrentSize, 'Z', WordSet); break; - case 5: CurrentSize = TokenizerPassRemoveHTMLStyle ( - BufferPntr, CurrentSize, 'Z', WordSet); break; - case 6: CurrentSize = TokenizerPassExtractURLs ( - BufferPntr, CurrentSize, 'Z', WordSet); break; - case 7: CurrentSize = TokenizerPassRemoveHTMLTags ( - BufferPntr, CurrentSize, 'Z', WordSet); break; - case 8: CurrentSize = TokenizerPassGetPlainWords ( - BufferPntr, CurrentSize, PrefixCharacter, WordSet); break; - default: break; - } - } - - delete [] BufferPntr; -} - - -/* 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); - - // Users need to be allowed to undo their mistakes... - m_IgnorePreviousClassification = true; - 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.spamdbm 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.spamdbm"); - 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 SpamDBM $Revision$\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"; - } - - if (g_BusyCursor != NULL) - SetCursor (g_BusyCursor); - - 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; - SetCursor (B_CURSOR_SYSTEM_DEFAULT); - 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; - SetCursor (B_CURSOR_SYSTEM_DEFAULT); -} - - -/* 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 << "SpamDBM 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. */ - g_QuitCountdown = 5; /* Quit more quickly */ - - 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, up to the maximum depth specified. */ - -status_t ABSApp::RecursivelyTokenizeMailComponent ( - BMailComponent *ComponentPntr, - const char *OptionalFileName, - set &WordSet, - char *ErrorMessage, - int RecursionLevel, - int MaxRecursionLevel) -{ - 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), - 'H' /* Prefix for Headers, uppercase unlike normal words. */, WordSet); - for (j = 0; j < 1000; j++) - { - HeaderValuePntr = ComponentPntr->HeaderField (HeaderKeyPntr, j); - if (HeaderValuePntr == NULL) - break; - AddWordsToSet (HeaderValuePntr, strlen (HeaderValuePntr), - 'H', 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, - 0 /* no prefix character, this is body text */, WordSet); - } - } - - /* Examine any sub-components in the message. */ - - if (RecursionLevel + 1 <= MaxRecursionLevel && - 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, - MaxRecursionLevel); - 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; - BEmailMessage WholeEMail; - - sprintf (ErrorMessage, "ABSApp::TokenizeParts: While getting e-mail " - "headers, had problems with \"%s\"", OptionalFileName); - - 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 /* Initial recursion level */, - (m_TokenizeMode == TM_JUST_HEADER) ? 0 : 500 /* Max recursion level */); - -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 more advanced. */ - -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 isn't all that useful a distinction, so do it. */ - - if (Letter >= 'A' && Letter < 'Z') - Letter = Letter + ('a' - 'A'); - - /* 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 && g_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 = "Word Database:"; - 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 = "Number of occurrences needed to store a word:"; - 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 = "Number of messages to store words from:"; - 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 = "Remove Old Words"; - 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; - m_PurgeButtonPntr->ResizeToPreferred(); - 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 = "Allow Retraining on a Message"; - 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 = "Print errors to Terminal"; - 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, " - Scan everything"); - break; - - case TM_PLAIN_TEXT: - strcat (TempString, " - Scan e-mail body text except rich text"); - break; - - case TM_PLAIN_TEXT_HEADER: - strcat (TempString, " - Scan entire e-mail text except rich text"); - break; - - case TM_ANY_TEXT: - strcat (TempString, " - Scan e-mail body text and text attachments"); - break; - - case TM_ANY_TEXT_HEADER: - strcat (TempString, " - Scan entire e-mail text and text attachments (Recommended)"); - break; - - case TM_ALL_PARTS: - strcat (TempString, " - Scan e-mail body and all attachments"); - break; - - case TM_ALL_PARTS_HEADER: - strcat (TempString, " - Scan all parts of the e-mail"); - break; - - case TM_JUST_HEADER: - strcat (TempString, " - Scan just the header (mail routing information)"); - 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, " - Learning Method 1: Naive Bayesian"); - break; - - case SM_CHISQUARED: - strcat (TempString, " - Learning Method 2: Chi-Squared"); - break; - - default: - break; - } -*/ - switch (ScoringMode) - { - case SM_ROBINSON: - strcpy (TempString, "Learning Method 1: Naive Bayesian"); - break; - - case SM_CHISQUARED: - strcpy (TempString, "Learning Method 2: Chi-Squared"); - 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 Spam Types", - 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", - "Default Settings", 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", - "Scan a 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", - "Train Spam Filter on a Message", - 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…", - 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), - "Haiku Spam Filter 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 << "SpamDBM shutting down..." << endl; - return 0; /* And implicitly destroys MyApp, which writes out the database. */ -} diff --git a/src/bin/bemail_utils/spamdbm.rdef b/src/bin/bemail_utils/spamdbm.rdef deleted file mode 100644 index 1b2306064d..0000000000 --- a/src/bin/bemail_utils/spamdbm.rdef +++ /dev/null @@ -1,136 +0,0 @@ -/* Resources for the spamdbm program, mostly icons and - file type associations that make it work properly. - $Id$ */ - -resource app_flags B_SINGLE_LAUNCH; - -resource app_signature "application/x-vnd.agmsmith.spamdbm"; - -resource app_version { - major = 3, - middle = 0, - minor = 0, - variety = B_APPV_FINAL, - internal = 1, - short_info = "SpamDBM $Rev$", - long_info = "SpamDBM - a spam analysis database manager for Haiku. $Id$" -}; - -resource file_types message { - "types" = "text/x-vnd.agmsmith.spam_probability_database", - "types" = "text/x-email" -}; - -resource(1, "Busy Cursor") #'CURS' array { - $"1001070707E018182084430242028279A005B9858DB180198109410A47022004" - $"181807E007E01FF83FFC7FFE7FFEFFFFFFFFFFFFFFFFFFFFFFFF7FFE7FFE3FFC" - $"1FF807E0" -}; - -resource(0, "BEOS:L:text/x-vnd.agmsmith.spam_probability_database") #'ICON' array { - $"FFFFFFFFFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFF00002A2A2A2A2A2A2A2A000000FFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFF00002A2A2A2A2A2A2A2A2A2A2A2A2A0000FFFFFFFFFFFFFF" - $"FFFFFFFFFFFF00002A2A2A2A00000000000000002A2A2A2A2A0000FFFFFFFFFF" - $"FFFFFFFFFF002A2A2A2A0000FFFFFFFFFFFFFFFF00002A2A2A2A2A00FFFFFFFF" - $"FFFFFFFF002A2A2A0000FFFFFFFFFFFFFFFFFFFFFFFF00002A2A2A2A00FFFFFF" - $"FFFFFF002A2A2A00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF002A2A2A2A2A00FFFF" - $"FFFFFF002A2A00FF00000000FFFFFFFFFFFFFFFFFFFF002A2A2A002A2A00FFFF" - $"FFFF002A2A00FFFF0000000000FFFFFFFFFFFFFFFF002A2A2A00002A2A2A00FF" - $"FFFF002A2A00FFFF0000FFFF0000FFFFFFFFFFFF002A2A2A00FF002A2A2A00FF" - $"FF002A2A00FFFFFF0000FFFFFF0000FFFFFFFF002A2A2A00FFFFFF002A2A00FF" - $"FF002A2A00FFFFFF0000FFFFFF0000FFFFFF002A2A2A00FFFFFFFF002A2A0000" - $"002A2A2A00FFFFFF0000FFFFFF0000FFFF002A2A2A00FFFFFFFFFFFF002A2A00" - $"002A2A00FFFFFFFF0000FFFFFF0000FF002A2A2A00FFFFFFFFFFFFFF002A2A00" - $"002A2A00FFFFFFFF0000FFFF0000FF002A2A2A00FFFFFFFFFFFFFFFF002A2A00" - $"002A2A00FFFFFFFF0000000000FF002A2A2A00FFFFFFFFFFFFFFFFFF002A2A00" - $"002A2A00FFFFFFFF00000000FF002A2A2A00FFFFFFFFFFFFFFFFFFFF002A2A00" - $"002A2A00FFFFFFFFFFFFFFFF002A2A2A000000000000FFFFFFFFFFFF002A2A00" - $"002A2A00FFFFFFFFFFFFFF002A2A2A00FF000000000000FFFFFFFFFF002A2A00" - $"002A2A00FFFFFFFFFFFF002A2A2A00FFFF0000FFFF0000FFFFFFFFFF002A2A00" - $"002A2A2A00FFFFFFFF002A2A2A00FFFFFF0000FFFF0000FFFFFFFF002A2A2A00" - $"FF002A2A00FFFFFF002A2A2A00FFFFFFFF0000000000FFFFFFFFFF002A2A00FF" - $"FF002A2A2A00FF002A2A2A00FFFFFFFFFF000000000000FFFFFFFF002A2A00FF" - $"FFFF002A2A00002A2A2A00FFFFFFFFFFFF0000FFFF0000FFFFFF002A2A2A00FF" - $"FFFF002A2A2A2A2A2A00FFFFFFFFFFFFFF0000FFFF0000FFFF002A2A2A2A00FF" - $"FFFFFF002A2A2A2A00FFFFFFFFFFFFFFFF000000000000FF002A2A2A2A00FFFF" - $"FFFFFF002A2A2A2A0000FFFFFFFFFFFFFF0000000000FF002A2A2A2A00FFFFFF" - $"FFFFFFFF002A2A2A2A000000FFFFFFFFFFFFFFFFFF00002A2A2A2A00FFFFFFFF" - $"FFFFFFFFFF002A2A2A2A2A000000000000000000002A2A2A2A2A00FFFFFFFFFF" - $"FFFFFFFFFFFF00002A2A2A2A2A2A2A2A2A2A2A2A2A2A2A2A2A0000FFFFFFFFFF" - $"FFFFFFFFFFFFFFFF00002A2A2A2A2A2A2A2A2A2A2A2A2A0000FFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFF" -}; - -resource large_icon array { - $"FFFFFFFFFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFF00002A2A2A2A2A2A2A2A000000FFFFFFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFF00002A2A2A2A2A2A2A2A2A2A2A2A2A0000FFFFFFFFFFFFFF" - $"FFFFFFFFFFFF00002A2A2A2A00000000000000002A2A2A2A2A0000FFFFFFFFFF" - $"FFFFFFFFFF002A2A2A2A0000FF000000FEFEFEFE00002A2A2A2A2A00FFFFFFFF" - $"FFFFFFFF002A2A2A0000000000FEFEFEFEFEFE00FEFE00002A2A2A2A00FFFFFF" - $"FFFFFF002A2A2A000000FEFEFEFEFEFEFE00FEFE000000002A2A2A2A2A00FFFF" - $"FFFFFF002A2A0000FEFEFEFEFEFEFE00FEFE00FEFEFE002A2A2A002A2A00FFFF" - $"FFFF002A2A00FEFEFEFEFEFE0000FEFE00FEFE00FE002A2A2A00002A2A2A00FF" - $"FFFF002A2A00FEFE00000000FEFE00FEFE00FE00002A2A2A00FE002A2A2A00FF" - $"FF002A2A00FEFEFEFE00FEFEFEFE00FEFE00FE002A2A2A00FEFEFE002A2A00FF" - $"FF002A2A0000FEFEFEFE00FEFE00FE00FEFE002A2A2A00FEFEFEFE002A2A0000" - $"002A2A2A00FF0000FEFE000000FEFEFE00002A2A2A00FEFEFEFEFEFE002A2A00" - $"002A2A00FFFFFF00FEFEFE00FEFEFEFE002A2A2A00FEFEFEFE00FEFE002A2A00" - $"002A2A00FFFFFFFF00FEFEFE00FE00002A2A2A00FEFE00FEFE00FEFE002A2A00" - $"002A2A00FFFFFFFFFF00FEFEFE00002A2A2A0000FEFE00FEFEFE00FE002A2A00" - $"002A2A00FFFFFFFFFF00FEFEFE002A2A2A0000FE00FE00FE00FE00FE002A2A00" - $"002A2A00FFFFFFFFFFFF00FE002A2A2A00FE00FE00FEFE000000FEFE002A2A00" - $"002A2A00FFFFFFFFFFFF00002A2A2A0000FE00FEFE00FE000000FEFE002A2A00" - $"002A2A00FFFFFFFFFFFF002A2A2A00FE00FEFE00FE00FE00FE00FE00002A2A00" - $"002A2A2A00FFFFFFFF002A2A2A00FE00FE00FE00FE00FEFEFEFEFE002A2A2A00" - $"FF002A2A00FFFFFF002A2A2A0000FE00FE00FEFE00FEFEFEFEFEFE002A2A00FF" - $"FF002A2A2A00FF002A2A2A00FEFE00FE0000FEFEFEFEFEFEFEFE00002A2A00FF" - $"FFFF002A2A00002A2A2A0000FEFE00FEFE00FEFEFEFEFEFE0000002A2A2A00FF" - $"FFFF002A2A2A2A2A2A00FF00FEFE00FEFEFEFEFEFE000000FF002A2A2A2A00FF" - $"FFFFFF002A2A2A2A00FF00FEFEFEFEFEFEFEFE0000FFFFFF002A2A2A2A00FFFF" - $"FFFFFF002A2A2A2A000000FEFEFEFEFEFE0000FFFFFFFF002A2A2A2A00FFFFFF" - $"FFFFFFFF002A2A2A2A000000FEFE000000FFFFFFFF00002A2A2A2A00FFFFFFFF" - $"FFFFFFFFFF002A2A2A2A2A000000000000000000002A2A2A2A2A00FFFFFFFFFF" - $"FFFFFFFFFFFF00002A2A2A2A2A2A2A2A2A2A2A2A2A2A2A2A2A0000FFFFFFFFFF" - $"FFFFFFFFFFFFFFFF00002A2A2A2A2A2A2A2A2A2A2A2A2A0000FFFFFFFFFFFFFF" - $"FFFFFFFFFFFFFFFFFFFF00000000000000000000000000FFFFFFFFFFFFFFFFFF" -}; - -resource(0, "BEOS:M:text/x-vnd.agmsmith.spam_probability_database") #'MICN' array { - $"FFFFFFFFFF0F2F2F2F2F0FFFFFFFFFFF" - $"FFFFFF0F2F2A2F2F2F2F2A2DD0FFFFFF" - $"FFFFD02A2F0FFFFFFFFF0F2F2A2DFFFF" - $"FF0F2AD00F0FFFFFFFFFFFD02A2D2FFF" - $"FF2F2FFF000F07FFFFFFD02AD02F2A0F" - $"0F2A0FFF00FF0F0FFFD02AD0FF0F2A07" - $"2F2DFFFF00FF0F0FD02AD0FFFFFF2F2F" - $"2F2FFFFF000F07D02AD0FFFFFFFF2F00" - $"2F2FFFFF0F0FD02A320F0FFFFFFF2F2F" - $"2F2FFFFFFFD02AD00F07070FFFFF2F2F" - $"D02A0FFFD02AD0FF0F0707FFFF0F2AD0" - $"FF2D2FD02AD0FFFF0F07070FFFD02A00" - $"FFD02A2AD0FFFFFF0F07070FD02A2DFF" - $"FFFF2D2A320FFFFFFF0F07D02A2DFFFF" - $"FFFFFF2F2A2D2F2F2F2F2D2A2D0FFFFF" - $"FFFFFFFF0F2F2F2F2F2F2FD0FFFFFFFF" -}; - -resource mini_icon array { - $"FFFFFFFFFFFF2F2F2F2FFFFFFFFFFFFF" - $"FFFFFFFF2A2A2A2A2A2A2A2DD0FFFFFF" - $"FFFFD02A2F00FFFFFEFEFF2F2A2DFFFF" - $"FFFF2A32FFFEFEFE00FEFE322A2D2FFF" - $"FF2F2AFEFEFEFE0000FED02AD02F2AFF" - $"FF2A07FEFE00FEFEFED02AD0FEFF2AFF" - $"2F2AFFFFFE0000FE322AD0FEFEFF2A2F" - $"2A2FFFFFFEFEFE322A32FE0000FE2F2A" - $"2A2FFFFFFEFED02AD0FEFEFE00FE2F2A" - $"2F2AFFFFFF322AD000FE0000FEFE2A2F" - $"FF2A0FFFD02A32FEFEFE00FEFEFF2AFF" - $"FF2D2FD02A32FE0000FEFEFEFF322AFF" - $"FFD02A2AD0FFFEFEFEFFFFFFD02A2DFF" - $"FFFF2D2A3207FEFFFFFFFFD02A2DFFFF" - $"FFFFFF2F2A2D2F2F2F2F2D2A2D0FFFFF" - $"FFFFFFFFFF2F2A2A2A2A2FFFFFFFFFFF" -};