spambayes-1.1a6/0000775000076500000240000000000011355064627013672 5ustar skipstaff00000000000000spambayes-1.1a6/CHANGELOG.txt0000664000076500000240000031264211116607110015713 0ustar skipstaff00000000000000[Note that all dates are in ISO 8601 format, e.g. YYYY-MM-DD to ease sorting] Release 1.1b1 ============= Skip Montanaro 2008-12-06 Start requiring Python >= 2.4. Skip Montanaro 2008-11-23 Route all pickle reads and writes through safepickle module. Skip Montanaro 2008-11-23 Pick off a bunch of pylint nit (still tons to do). Release 1.1a5 ============= Skip Montanaro 2007-07-14 Add train and train_mime methods to XML-RPC plugin (from Marian Neagul). Skip Montanaro 2007-07-15 Switch repository from CVS to Subversion. Release 1.1a4 ============= Skip Montanaro 2006-09-09 Dump NetPBM decode support in favor of PIL Skip Montanaro 2006-09-09 First crack at handling image sequences Skip Montanaro 2007-05-11 IMAP patch for contrib/tte.py (Dave Abrahams) Skip Montanaro 2007-05-11 Remove duplicate use of --cullext flag to contrib/tte.py Skip Montanaro 2007-05-22 Note missing file name in error message - FileStorage.py Skip Montanaro 2007-05-24 Set MinTTL to one day in dnscache.py Skip Montanaro 2007-05-25 Catch broader exception in ImageStripper.py when image load fails (Sjoerd Mullender) Skip Montanaro 2007-06-10 Add core_server.py & friends - plugin-based server Release 1.1a3 ============= Skip Montanaro 2006-08-18 Update pycksum.py to try and identify more duplicates Skip Montanaro 2006-08-14 Add scale and charset options to ImageStripper Skip Montanaro 2006-08-13 Stitch spam images back together properly, add a couple more tokens Skip Montanaro 2006-08-10 Add support for PIL to ImageStripper.py Skip Montanaro 2006-08-09 Cache x-lookup_ip in a pickle instead of trying to use anydbm or zodb Skip Montanaro 2006-08-06 Add crude OCR capability to try and parse image-based spam using Ocrad & NetPBM Skip Montanaro 2006-08-06 Add x-short_runs option Skip Montanaro 2006-08-06 Add x-image_size option & corresponding token Skip Montanaro 2006-08-06 Add Matt Cowles' x-lookup_ip extension w/ slight modifications Skip Montanaro 2006-08-06 Add profiling using cProfile (if available) to sb_filter.py Skip Montanaro 2006-08-06 Delete -d and -p flags from spamcounts.py Skip Montanaro 2006-08-06 Refactor basic text tokenizing out of tokenize_body into a separate method, tokenize_text Skip Montanaro 2006-08-05 Explicitly close ZODB store in tte.py Tony Meyer 2006-06-22 Fix bug in regex preventing valid IPs Toby Dickenson 2006-06-12 Suppress spurious duplicate From_ lines in sb_bnfilter.py Tony Meyer 2006-06-10 Add simple parts of [ 824651 ] Multibyte message support Tony Meyer 2006-05-06 Enable -o command line option setting, and follow TestDriver directories in testtools/mksets.py Skip Montanaro 2006-04-23 Reduce sensitivity of spamcounts.py to classifier changes Tony Meyer 2006-04-22 Set zodb cache size to 10,000 Release 1.1a2 ============= Tony Meyer 2006-04-03 Add [ 1081787 ] Adding the version only to sb_filter.py Tony Meyer 2006-04-03 Fix [ 1383801 ] trustedIPs wildcard to regex broken Tony Meyer 2006-04-02 Fix [ 1387699 ] train_on_filter=True needs the db to be opened read/write Tony Meyer 2006-04-02 Fix [ 1387709 ] If globals:dbm_type is non-default, then don't use whichdb. Tony Meyer 2005-11-27 Install the conversion utility and offer to run it on Windows install. Tony Meyer 2005-11-26 Add conversion utility to easily convert dbm to ZODB. Tony Meyer 2005-11-26 Change database names to hammie.fs and messageinfo.fs Tony Meyer 2005-11-26 Have ZODBClassifier honour read-only mode. Tony Meyer 2005-11-26 Change default storage to ZODB. Tony Meyer 2005-11-26 Remove backwards compatibility option to use True for dbm and False for pickle. Tony Meyer 2005-11-26 Fix closing SSL connections in POP3 proxy. Tony Meyer 2005-11-26 Abort failed ZODB transactions. Tony Meyer 2005-11-15 Fix exception header adding. Tony Meyer 2005-11-15 Tidy up the way that messages keep track of the messageinfo database. Tony Meyer 2005-11-15 Add ZODB version of MessageInfo class. Tony Meyer 2005-11-15 Let the notate_ options be turned off. Tony Meyer 2005-11-15 Add simple handling of ZODB ConflictErrors. Tony Meyer 2005-11-15 Pack ZODB database. Tony Meyer 2005-11-15 Fix tokenizer so that the -o switch can be used with all options. Tony Meyer 2005-11-15 Only run through expiry if messages might expire. Richie Hindle 2005-11-15 Change default action for review page to discard for ham and spam. Tony Meyer 2005-07-08 imapfilter: If a folder (e.g. ham_folder) isn't selected, then don't try and expunge it. Sjoerd Mullender 2005-07-03 imapfilter: It seems it is possible that we get an invalid date string from self.extractTime(), so protect against that. Mark Hammond 2005-06-21 Update Outlook folder dialogs so we work with new and old pywin32 versions. Tony Meyer 2005-06-03 Use has_key() and not "in" for Python 2.2 compat. Skip Montanaro 2005-06-03 sb_filter: If database doesn't exist, initialize it. Closes the ancient #759917. Sjoerd Mullender 2005-05-30 imapfilter: After closing a folder, there is no current folder, so set current_folder to None, avoiding a crash on close. Tony Meyer 2005-05-26 Use get_content_type instead of the deprecated get_type Tony Meyer 2005-05-22 ProxyUI: Fix the handling of the notate_to and notate_subject options. Sjoerd Mullender 2005-05-19 imapfilter: In each iteration, create a new IMAPSession object and hence a new connection to the server. Tony Meyer 2005-05-13 Fix [ 1182754 ] 1.1a1: imapfilter browser chokes on incorrect password Tony Meyer 2005-04-26 Add ability to handle unsures to fpfn.py Tony Meyer 2005-04-26 Add [ 618932 ] fpfn.py: add interactivity on unix Tony Meyer 2005-04-22 Fix [ 1182671 ] When cache directories are full, 1.1a1 starts slowly Tony Meyer 2005-04-22 Fix [ 1187208 ] import into CDB chokes on 8-bit chars Tony Meyer 2005-04-21 Use the os.path.basename as the database name for a ZODB FileStorage database, so that if the database is moved it will still work. Tony Meyer 2005-04-21 Add [ 1182703 ] sb_imapfilter binary should output to log Skip Montanaro 2005-04-19 tte.py: Include time to create & reverse mailbox in accounting for each round Tony Meyer 2005-04-14 Fix [ 1179055 ] minor UI correction needed (1.1.a1) Tony Meyer 2005-04-14 Highlight current row of review table with a little bit of javascript. Tony Meyer 2005-04-13 Fix [ 1181160 ] Language fallback doesn't work Tony Meyer 2005-04-07 Add basic options to sort+group.py Release 1.1a1 ============= Tony Meyer 2005-03-16 Optionally load configuration file for each imap server. Tony Meyer 2005-03-09 Use a blocking socket when connecting with sb_server, so that with Python 2.4 we don't end up with never-ending reports of errors. Tony Meyer 2005-02-23 Add [ 1144670 ] SMTP slow on large files Tony Meyer 2005-02-14 Handle multiple imap servers. Adds [ 1122067 ] Feature Request: Config sb_imapfilter for multiple accounts Tony Meyer 2005-02-14 New script: A little script that outputs to stdout a 'show clues' report like the one that Outlook creates, with the clues, tokens and message stream. Kenny Pitt 2005-02-12 Add a "Notifications" tab to SpamBayes Manager for configuring notification sounds. Tony Meyer 2005-02-08 Move the ensureDir function to storage, as many scripts use it. Tony Meyer 2005-02-01 Update setup script to use Inno Setup 5.x rather than 4.x (patch from Kenny Pitt) Tony Meyer 2005-01-28 Distribute mscvr71.dll, as Microsoft recommend. Tony Meyer 2005-01-28 The 'fix' for escaping subject lines was incorrect; do it correctly. Tony Meyer 2005-01-24 Add new part of [ 1106457 ] bsddb185 has to be covered in dbmstorage.py Tony Meyer 2005-01-21 Fix [ 1106457 ] bsddb185 has to be covered in dbmstorage.py Tony Meyer 2005-01-17 Add a method to generate documentation for options that suits a HTML documentation file. Tony Meyer 2005-01-17 OptionsClass: Add an optional flag add_comments to display(). If this is True (False by default) then the help string for the option is added in comments above the option. Tony Meyer 2005-01-17 Outlook: Remove migrating the data directory - the need for this predates any of the combined binaries, so should be past now. Tony Meyer 2005-01-17 Outlook: Look for a {profile name}_bayes_customize.ini file in the data directory, and add that (lastly) to the spambayes configuration files to load. This allows per-profile spambayes config changes, and makes things more consistent with the Outlook options. Tony Meyer 2005-01-14 Try and work around the reasonably common problems with imaplib and OS X. Kenny Pitt 2005-01-12 Outlook: Now that we have more space in the Manager dialog, split the "Filter status" info into separate lines for better readability. Also add information about the setting of the new good message folder option. Kenny Pitt 2005-01-12 Outlook: General dialog template cleanup. Kenny Pitt 2005-01-08 Since bsddb is the same as bsddb3 on any recent Python (2.3+), try using bsddb3 instead if we are unable to import bsddb. Tony Meyer 2005-01-04 If we are reloading the options, optionsPathname will already be set, so if we are a windows user using the default pathname, the object won't be reloaded. Fix so a reload is always done. Tony Meyer 2005-01-04 rcparser.py: Look for dialogs.h in this directory if it isn't with dialogs.rc. Tony Meyer 2005-01-04 Add initial French and Spanish translations. Tony Meyer 2005-01-03 Implement part of [ 753708 ] Support POP over SSL Tony Meyer 2005-01-02 UserInterface.py: cgi.escape the configuration filename Skip Montanaro 2005-01-02 tte.py: When running through the messages that haven't been kept for training, delete messages that score properly. Tony Meyer 2004-12-29 Outlook: Report the last modified date in the "show clues" message. Kenny Pitt 2004-12-24 New version numbering scheme. Tony Meyer 2004-12-23 Outlook: Enlarge the Manager dialog and add a control to set the ham folder. Tony Meyer 2004-12-23 Provide constants for the persistent ham/spam/unsure stings for the messageinfo db. Kenny Pitt 2004-12-23 Add a button on the Statistics tab to reset the Outlook statistics. Also display the date when the statistics were last reset. Tony Meyer 2004-12-22 Make Dibbler.py compatible with Python 2.2.1. Tony Meyer 2004-12-22 FileCorpus.py: Don't use the strict keyword arg as it is deprecated. Tony Meyer 2004-12-22 Add _() wrappers around appropriate strings in spambayes/ and scripts/. Tony Meyer 2004-12-22 i18n.py: Add a function to load the ui.html file appropriate for the language. Tony Meyer 2004-12-22 Merge the Outlook2000.oastats.Stats class and the spambayes.Stats.Stats class. Tony Meyer 2004-12-22 Store a stats start date in the message database, so that we can 'reset' the statistics without removing the whole message database. Tony Meyer 2004-12-22 Allow message.Messages to have their id specified on creation. Also allow specification of the messageinfo_db. Kenny Pitt 2004-12-21 sb_server: Fix a bug with the new style of checking the +OK on a RETR. Tony Meyer 2004-12-20 Allow Outlook users to select their storage method in exactly the same way as other users can. Tony Meyer 2004-12-20 Outlook: add a wrapper for the ZODB (FileStorage) storage class. Tony Meyer 2004-12-20 Outlook: Remove some checks that no longer need to be done (that the messageinfo db has the same length as the tokens db). Tony Meyer 2004-12-17 message.py: Spit out a warning if the deprecated function is used. Tony Meyer 2004-12-17 message.py: Change the deprecated function to work in Python 2.4. Tony Meyer 2004-12-17 sb_imapfilter: Update extract_fetch_data to handle any number of literals/message numbers. Kenny Pitt 2004-12-17 Outlook: Improve e-mail address formatting for fake Exchange headers so that it supports multiple addresses, some of which may be real Internet addresses. Tony Meyer 2004-12-16 sb_imapfilter: compile re's and do this only once. Tony Meyer 2004-12-16 sb_imapfilter: improve handing of extract_fetch_data. Tony Meyer 2004-12-16 Outlook: Improve the faked up exchange headers that we generate if there are no Internet headers. Kenny Pitt 2004-12-16 Outlook: Include Message-ID in fake Exchange headers. Tony Meyer 2004-12-08 Improve test_storage.py and test_message.py. Tony Meyer 2004-12-08 Add [ 848365 ] Remove subject annotations from message review page Tony Meyer 2004-12-08 Add [ 1036970 ] Allow Outlook plugin to move ham to a designated folder Tony Meyer 2004-12-07 Outlook: More detailed statistics in SpamBayes Manager. These roughly match the updated statistics in the sb_server Web UI. Tony Meyer 2004-12-06 Fix the regex that the auth digest used, and handle the auth digest responses tha IE 6.0 and Firefox 1.0 give. Tony Meyer 2004-12-06 Fix [ 1078923 ] Unicode support incomplete Tony Meyer 2004-12-06 Outlook: Use PR_SENDER_NAME rather than PR_DISPLAY_NAME_A for the faked-up "From" header. Kenny Pitt 2004-12-04 Add notification sound support as per patch #858925. Tony Meyer 2004-12-01 When doing "setup.py sdist" an MD5 checksum and the size of the created archive(s) is printed to stdout. Tony Meyer 2004-11-29 sb_server: Messages that did not have the required \r?\n\r?\n separator would just pass through spambayes unproxied. Now they are, as best as possible. Tony Meyer 2004-11-29 Handle a message that does not have a proper separator in insert_exception_header. Tony Meyer 2004-11-29 Change sb_server to use the centralised insert_exception_header code. Tony Meyer 2004-11-29 Subject lines are not cgi.escape()d in the web interface, which might cause errors - fixed. Tony Meyer 2004-11-26 Outlook: Stop using the deprecated access to the bayes database and use the manager.classifier_data directly. Tony Meyer 2004-11-26 Outlook: Switch to using a spambayes.message.MessageInfo database rather than an Outlook specific one. Tony Meyer 2004-11-26 Outlook: Save the current folder when doing a "delete as spam", because the message may not be in the folder it was when it was filtered, or it may not have been filtered, but we do really want to recover it to wherever it was last. Tony Meyer 2004-11-26 Fix [ 1071319 ] Outlook plug in for IMAP boxes Tony Meyer 2004-11-26 message.py: Handle loading an Outlook messageinfo database, and add a __len__ function to the messageinfo databases. Tony Meyer 2004-11-26 message.py: The messageinfo db now needs messages to have a GetDBKey function to determine the key to store the message under. For non-Outlook message classes, this is just the same as getId(). Tony Meyer 2004-11-24 Moved the cleanarch script to the utilities directory and added '.py' to the filename. Tony Meyer 2004-11-23 Improve OE support code, mostly from [ 800671 ] Windows GUI for easy Outlook Express mailboxes training Tony Meyer 2004-11-23 message.py: Change MessageInfoBase's methods so that recording & retrieving a message are not private methods and are more clearly named. Tony Meyer 2004-11-23 message.py: Change so that the messageinfodb doesn't get created/opened on import, but rather through utility functions like those in spambayes.storage. Tony Meyer 2004-11-23 message.py: Remove the asTokens method in favour of the existing tokenize function. Tony Meyer 2004-11-23 message.py: Fix the include_evidence header to check for *H* and *S* explicitly rather than any token starting with *. Tony Meyer 2004-11-22 Add new storage types: CBDClassifier, ZODBClassifier, ZEOClassifier Tony Meyer 2004-11-22 Add code to allow persistent_storage_name to not be expanded into an absolute path with certain storage types (e.g. the SQL ones). Tony Meyer 2004-11-22 sb_pop3dnd: Play nicer with win32 gui Tony Meyer 2004-11-22 sb_pop3dnd: Don't use the deprecated 'strict' kwarg for email messages. Tony Meyer 2004-11-22 sb_pop3dnd: Add appropriate state createworkers function & call. Tony Meyer 2004-11-22 sb_pop3dnd: Modify to have the prepare/start/stop API that sb_server has. Tony Meyer 2004-11-22 sb_filter: Remove the "experimental" marking in the docstring for the training functions. Tony Meyer 2004-11-15 Fix a bug in sb_dbexpimp.py where merging into an existing dbm file might lose training data. Tony Meyer 2004-11-15 sb_dbexpimp.py: Fail if the csv file doesn't exist that we are trying to import from rather than keeping going, which made no sense. Tony Meyer 2004-11-15 sb_dbexpimp.py: Stop bothering to remove the .dat and .dir files that dumbdbm create (long time since they were supported), and remove the verbose flag, which doesn't actually do anything. Kenny Pitt 2004-11-12 Add a separate Statistics tab to make room for more detailed statistics. Toby Dickenson 2004-11-11 Add a version of sb_bnfilter in C (for speed). Tony Meyer 2004-11-11 The installer wasn't offered to install a startup items shortcut, so fix that. This is a non-ideal patch, but appears to be the only way Inno will work. Tony Meyer 2004-11-09 Implement [ 870524 ] Make the message-proxy timeout configurable Tony Meyer 2004-11-09 Use email.message_from_string(text, _class) rather than our wrapper functions. Tony Meyer 2004-11-09 Implement [ 940547 ] imapfilter interface available when using -l switch Tony Meyer 2004-11-08 Outlook: Add two extra items to the "spam clues" for the message: last filtered score/class and if it has been trained. Tony Meyer 2004-11-05 Add unittests for sb_pop3dnd.py Tony Meyer 2004-11-05 sb_pop3dnd: remove use of the web interface Tony Meyer 2004-11-05 sb_pop3dnd: fix bug in getHeaders where negation wouldn't work correctly Tony Meyer 2004-11-05 sb_pop3dnd: fix loading of dynamic messages to correctly generate the headers, so that envelope works. Tony Meyer 2004-11-05 sb_pop3dnd: change the fake email addresses to the same format as the notate_to option (i.e. @spambayes.invalid) Tony Meyer 2004-11-05 sb_pop3dnd: improve the "about" message to include the docstring. Tony Meyer 2004-11-05 sb_pop3dnd: add a dynamic stats message. Tony Meyer 2004-11-05 sb_pop3dnd: improve the dynamic status message to include everything that would normally be on the web interface. Tony Meyer 2004-11-05 sb_pop3dnd: add a "train as spam" folder, to separate out training and classifying as spam. Tony Meyer 2004-11-05 sb_pop3dnd: use twisted.Application in the new style to avoid deprecation warnings. Tony Meyer 2004-11-03 Add [ 1052816 ] I18N - mostly the patch from Hernan Martinez Foffani Tony Meyer 2004-11-03 Fix [ 1022848 ] sb_dbexpimp.py crashes while importing into pickle file Tony Meyer 2004-11-03 Fix [ 831864 ] sb_mboxtrain.py: flock vs. lockf Tony Meyer 2004-11-03 Fix [ 922063 ] Intermittent sb_filter.py failure with URL pickle Tony Meyer 2004-11-03 Outlook: Also add an "X-Exchange-Delivery-Time" header to the faked up Exchange headers. Tony Meyer 2004-11-02 Improve the web interface statistics Tony Meyer 2004-10-29 If possible, use the builtin (faster, C-implemented) set class, falling back to sets.Set, then back to our compatsets.Set Tony Meyer 2004-10-28 Add [ 715248 ] Pickle classifier should save to a temp file first Tony Meyer 2004-10-28 Add [ 938992 ] Allow longer background filtering delays Tony Meyer 2004-10-27 Add a variety of improvements to sb_culler.py contributed by Andrew Dalke Tony Meyer 2004-10-27 Update sb_culler.py to match current open_storage() usage Tony Meyer 2004-10-21 Fix [ 1051081 ] uncaught socket timeoutexception slurping URLs Tony Meyer 2004-10-20 Outlook: Let the statistics have a variable number of decimal places for the percentages (1 by default). Tony Meyer 2004-10-18 Make msgs.Msg objects pickleable Tony Meyer 2004-10-18 Copy Skip's -o command line option (available in all the regular scripts) to timcv.py. Tony Meyer 2004-10-18 TestDriver: If show_histograms was False, then the global ham/spam histogram never had the stats computed, but this gets used later, so the script would die with an AtrributeError. Fix that. Tony Meyer 2004-10-15 Outlook: Add persistent statistics Tony Meyer 2004-10-13 Implement [ 1039057 ] Diffs for IMAP login problems... Tony Meyer 2004-10-13 Add Classifier.use_bigrams option to the Advanced options page for sb_server and imapfilter. Tony Meyer 2004-10-13 Fix mySQL storage option for the case where the server does not support rollbacks. Tony Meyer 2004-10-07 Add patch from Graham Ashton to allow users of sb_upload.py to not just upload, but also train. Sjoerd Mullender 2004-10-02 imapfilter: Quote the search string that tries to find the message again that was just saved. Kenny Pitt 2004-10-02 Outlook: Change "Delete as Spam" button to "Spam" and "Recover from Spam" button to "Not Spam". Tony Meyer 2004-10-01 Instead of treating notate_to just like notate_subject, we convert the classification into an email address. Tony Meyer 2004-09-30 Implement [ 940643 ] Add ham_folder option Tony Meyer 2004-09-30 Fix [ 903905 ] IMAP Configuration Error Tony Meyer 2004-09-29 Fix [ 1036601 ] typo on advanced config web page Tony Meyer 2004-09-15 sb_upload: Clarify docstring so that it's more clear what this script does. The -n / --null command line option didn't actually do anything; change it so that it does. Sjoerd Mullender 2004-08-20 imapfilter: Fix the regular expression to match the Message-ID header by stopping on newline. Skip Montanaro 2004-08-18 tte.py: Seems better to try and alternate ham/spam scoring instead of scoring all the hams in a batch and all the spams. Kenny Pitt 2004-08-11 First pass at moving help text out of the Python source and into the ui.html file. Tony Meyer 2004-08-10 Warn people using spam_cutoff/ham_cutoff values of 0.5 or lower/higher. Also warn them if the ham_cutoff is higher than the spam_cutoff. Tony Meyer 2004-08-09 Change [Classifier] x-use_bigrams to a normal, not experimental option. Tony Meyer 2004-08-06 imapfilter: isinstance check is wrong, so will never be true, so literals in the folder list will never be handled correctly. Tony Meyer 2004-08-05 Remove all traces of the experimental imbalance option. Tony Meyer 2004-08-05 Remove support code for two deprecated options: [Tokenizer] x-extract_dow and [Tokenizer] x-generate_time_buckets. Tony Meyer 2004-08-04 Add basic unit tests for imapfilter. Tony Meyer 2004-08-04 Factor out X-Spambayes-Exception header code to message.py, and get imapfilter to use this. Tony Meyer 2004-08-04 imapfilter: Keep going if just one folder is bad (training/filtering). Tony Meyer 2004-08-04 imapfilter: Switch to using the Message-ID header id as our id, unless one can't be found, in which case we use our one. Tony Meyer 2004-08-04 imapfilter: General sb_imapfilter tidy-up. Tony Meyer 2004-08-04 imapfilter: Remove the layers of attempting to fetch. Tony Meyer 2004-08-04 imapfilter: Change the way the get_substance method works (renaming it in the process). Tony Meyer 2004-08-04 imapfilter: Be less restrictive about the error returned when logging in fails. Sjoerd Mullender 2004-08-03 message.py: Don't round-trip the message being tokenized to a string. Tony Meyer 2004-08-03 Implement [ 909088 ] remove STLS pop3 capability Skip Montanaro 2004-07-26 tte.py: Generalize the spam:ham ratio flag to include the ham value instead of having it be implicitly 1. Skip Montanaro 2004-07-25 tte.py: Add --ratio=N flag to allow the user to adjust the ratio of spam to ham. Tony Meyer 2004-07-23 For proxy handler for version checking, the proxy port needs to be an integer, not a string. Tony Meyer 2004-07-22 pop3proxy_tray: Do what the Outlook plug-in does and give the user the "string" version of the version number. Tony Meyer 2004-07-22 Fix proxy handler for checking for latest version when username/password isn't given. Fix for Python 2.4 Kenny Pitt 2004-07-22 Improve display names for "allow_remote_connections" options to be less confusing. Tony Meyer 2004-07-19 Fix [ 990700 ] Changes to asyncore in Python 2.4 break ServerLineReader Kenny Pitt 2004-07-17 Add an "Empty Spam Folder" option to the plug-in dropdown menu. (Patch [831941]) Kenny Pitt 2004-07-17 Fix [941639] and [986353]. Use a non-standard extension for our py2exe created zip to get around Windows extensions that automatically expand zip files. Tony Meyer 2004-07-16 Fix [ 943852 ] Incorrect sort order for Score column Tony Meyer 2004-07-16 Implemented [ 887984 ] Remove "Save and Shutdown" button when running as service Tony Meyer 2004-07-14 Fix [ 790757 ] signal handler created with wrong # of args Tony Meyer 2004-07-14 Fix [ 944109 ] notate_to/subject option valid values should be dynamic Tony Meyer 2004-07-14 Fix [ 959937 ] "Invalid server" message not always correct Tony Meyer 2004-07-14 When slurping, use a lower timeout so things work faster (with Python >=2.3) Tony Meyer 2004-07-14 setPayload() in message.py doesn't work in Python 2.4, so avoid. Refactor Corpus.py and FileCorpus.py to allow this. Skip Montanaro 2004-07-10 tte.py: 2.3 compatibility: add reversed() function Skip Montanaro 2004-07-10 tte.py: When deciding if messages are misses or not, consider whether they have Message-Id or Subject headers. Tony Meyer 2004-07-09 Using -u with sb_server had been broken. Fix this. Tony Meyer 2004-07-09 Update test_storage.py test to reflect (current) correct way to call open_storage. Fixes part of [ 981970 ] tests failing. Tony Meyer 2004-07-04 Add a time stamp to the Outlook plug-in logs. Tony Meyer 2004-07-04 Fix [ 933473 ] Unnecessary spam folder hook. Neil Schemenauer 2004-06-30 New script, hammie2cdb.py, that converts hammie databases into cdb databases (usable by CdbClassifier). Skip Montanaro 2004-06-29 tte.py: Worm around the extremely rare case during verbose mode where the message sneaks through without either a message-id or a subject. Skip Montanaro 2004-06-26 New script, postfixproxy.py, a first cut proxy filter for use with PostFix 2.1's content filter stuff. Skip Montanaro 2004-06-26 hammie: Rename filter() to score_and_filter() and return both the spamprob and the modified message. Tony Meyer 2004-06-11 Add the "sb_culler.py" script from the SB wiki by Andrew Dalke. Tony Meyer 2004-06-01 Add fixes from [ 962693 ] mozilla/thunderbird/netscape profile dir detection Skip Montanaro 2004-05-26 New file: pycksum.py - A fuzzy checksum program similar based on a message posted a long time ago by Justin Mason of the SpamAssassin group. Tony Meyer 2004-05-24 imapfilter: With the verbose output, put the final counts on a line all by themselves. Tony Meyer 2004-05-16 imapfilter: Sometimes we can't fetch a message, so we can't add the exception header. Don't crash, just warn and continue. Toby Dickenson 2004-05-06 Add a C implementation of sb_bnfilter. Tweak the Python version to minimise differences between the two. Enable psyco in sb_bnserver. Release 1.0 Final ================= (no changes from 1.0rc2) Release 1.0 Release Candidate 2 =============================== Tony Meyer 2004-06-10 Install license with binaries. Mark Hammond 2004-06-01 Fix Tony's fix for [ 943397 ] Cannot choose more than one Inbox folder to filter Tony Meyer 2004-06-02 Fix [ 961713 ] server error when trying to change Statistics Options Skip Montanaro 2004-05-31 Compatcsv: Abandon regexes as bad way to parse csv file - add simple unit test as well Skip Montanaro 2004-05-31 sb_dbexpimp.py: handle case of importing the old object craft csv module. Skip Montanaro 2004-05-22 sb_dbexpimp.py: Add a fake UnicodeDecodeError for Python 2.2 compatibility. Skip Montanaro 2004-05-22 Compatcsv: correct a list chomping bug (forgot a slicing ":") and use .readline() instead of .next() for Python 2.2 compatibility Tony Meyer 2004-05-18 Fix [ 955442 ] default_bayes_customize.ini not handled right Tony Meyer 2004-05-14 message.py: If we trigger the TypeError problem, try to do our best to put the message together anyway. Tony Meyer 2004-05-14 Handle people using a storage type that isn't pickle/dbm (like one of the sql ones). Tony Meyer 2004-05-13 If a user tried to send a bug report before entering in any servers to proxy, this would generate an IndexError. Fix that. Tony Meyer 2004-05-12 Fix [ 943397 ] Cannot choose more than one Inbox folder to filter Release 1.0 Release Candidate 1 =============================== Skip Montanaro 2004-05-05 sb_filter, sb_bnfilter use new mboxutils as_string(). Skip Montanaro 2004-05-04 mboxutils: Added as_string() - a function which attempts to work around problems flatten()-ing some Message objects in versions of the email package < 2.5.5. Mark Hammond 2004-05-04 Fix [ 918157 ] "Could not watch the specified folders". msgstore.IsReceiveFolder() would fail on an exchange server. Tony Meyer 2004-05-03 Temporary fix for [ 941596 ] sb_imapfilter.py not adding headers / moving messages Tony Meyer 2004-05-03 imapfilter: catch any exceptions, not just email.Errors Tony Meyer 2004-05-03 NotesFilter: Incorrect (old?) function name for opening a storage object. Mark Hammond 2004-04-29 Outlook: Change the default pushbutton in the "filter now" dialog to be the start button, and make ESC work as cancel. Skip Montanaro 2004-04-28 tte.py: add -R/--reverse flag to iterate over the mailboxes in reverse Tony Meyer 2004-04-25 If the move_trained_[ham|spam]_to_folder options were used, this would wipe out the SpamBayes headers. Fix that. Tony Meyer 2004-04-25 Update message.py for the newer way that the persistent_use_database option works. Tony Meyer 2004-04-23 Fix [ 940155 ] Data Folder location Release 1.0 Beta 1 ================== Kenny Pitt 2004-04-07 Fix some problems with the [851785] fix Toby Dickenson 2004-03-26 New script: sb_bnfilter - like sb_filter but without the startup overhead Tony Meyer 2004-03-24 When messages expired from the sb_server caches during a USER check (rather than on launch) they would be untrained. This is not right at all, and so was fixed. Tony Meyer 2004-03-23 Fix a subtle bug where if one option was selected for notate_to/subject the option would be presented with radio buttons not checkboxes (so only one, and never zero or 2/3 options could be chosen). Kenny Pitt 2004-03-18 Outlook: Don't record classification in stats unless all_actions is true so that rescoring messages doesn't skew the statistics counters. Skip Montanaro 2004-03-16 Change sb_dbexpimp.py to use csv as interchange format. Tony Meyer 2004-03-16 Add [ 797579 ] Disable connections to POP3 and SMTP from remote hosts Tony Meyer 2004-03-16 Fix [851785] Pop3proxy stores Ham/Spam/Unsure subject line in message cache Tony Meyer 2004-03-16 Fix [ 906581 ] Assertion failed in search subject Tony Meyer 2004-03-16 Add a note warning about [915466] Sorting review page loses classifications. Skip Montanaro 2004-03-16 Add a verbose flag to tte.py Mark Hammond 2004-03-07 Outlook: Catch all MAPI errors fetching the HTML for a message, and remove the warning about old win32all versions. Mark Hammond 2004-03-04 Outlook: set pythoncom.frozen along with sys.frozen in our nasty registration hacks Mark Hammond 2004-02-27 Outlook: Handle the fact that GetParent() may raise an exception, in which case we aren't able to show the item in the tree. Mark Hammond 2004-02-27 Outlook: GetParent() catches MAPI errors and raises a MsgStoreException Mark Hammond 2004-02-27 Outlook: Improve speed by calling .SetColumns() before .Restrict() Skip Montanaro 2004-02-26 Add a -c flag to tte.py Tony Meyer 2004-02-22 Simplify the auto bug reports via the web interface a bit, and get the user to enter a subject. Tony Meyer 2004-02-17 Add [ 848311 ] sb_imapfilter.py obeys launch_browser Tony Meyer 2004-02-17 The service was built, but not included in the installer for 1.0a9. Fix that. Tony Meyer 2004-02-16 Fix the line wrapping in autogenerated bug reports via the web interface. Tony Meyer 2004-02-16 Fix a NameError in smtpproxy.py Tony Meyer 2004-02-16 imapfilter: Use BODY.PEEK[] instead of RFC822.PEEK. Tony Meyer 2004-02-16 imapfilter: Report time taken a little less pedantically. Tony Meyer 2004-02-16 Fix [ 737967 ] Malformed messages break pop3proxy (et al) Tony Meyer 2004-02-15 Fix half of [ 896366 ] Crashes in the web interface. Skip Montanaro 2004-02-13 tte.py: record time (in seconds) to execute each round and count the number of leftover hams and spams at the end Skip Montanaro 2004-02-13 Collect all potential MTA complaints, not just sendmail's "may be forged" (from Tim Peters). Tony Meyer 2004-02-13 Fix [ 895606 ] 1.0a9 proxy raises an X-Spambayes-Exception Skip Montanaro 2004-02-12 Big speedup when using sb_filter.py to process an entire mailbox. Instead of opening the database for each filter operation, cache the open db object and reuse as long as its mode is the same as the last time we used it. Kenny Pitt 2004-02-11 Fix typo in sb_dbexpimp.py usage statement. Release 1.0 Alpha 9 =================== Skip Montanaro 2004-02-07 New script - tte.py: Train to exhaustion based upon my understanding of the technique as described in Gary Robinson's blog. Tony Meyer 2004-02-05 Web interface: added a third configuration page, which dynamically has all the experimental options (not the deprecated ones). Tony Meyer 2004-02-05 Make all the scripts consistent in their command line setting of database type/name. -d is dbm, -p is pickle. Also setup things nicer for mysql and pgsql. Tony Meyer 2004-02-05 If the allowed values for an option is a tuple of values, then the type is correct if the value is one of those types. Tony Meyer 2004-02-05 Outlook: If the special folders can't be found, then report this to the user. Tony Meyer 2004-02-05 ProxyUI: Do the save one line earlier, which might mean that if the user has closed the browser, a db error is avoided. Tony Meyer 2004-02-05 imapfilter has been adding two mailid headers, so stop that. Tony Meyer 2004-02-05 Fix [ 890645 ] imapfilter: "ValueError: year out of range" Skip Montanaro 2004-01-30 Recognize "abbreviated" URLs of the form www.xyz.com or ftp.xyz.com as http://www.xyz.com and ftp://ftp.xyz.com, respectively. This gets rid of some fairly common "skip:w NNN" tokens. Enabled by the new tokenizer option, x-fancy_url_recognition. Skip Montanaro 2004-01-29 sb_mboxtrain.py: preserve modtimes in Maildir & MH mailboxes Tony Meyer 2004-01-27 Fix [ 870799 ] imap trying to fetch invalid message UID Tony Meyer 2004-01-27 Fix [ 881427 ] sb_mboxtrain.py requires -d or -D Tony Meyer 2004-01-27 Add options to ImapUI that the ProxyUI has, and also belong there. Kenny Pitt 2004-01-22 Outlook: Fix confusing error log message. Timer delay values are configured in seconds, not milliseconds. Skip Montanaro 2004-01-21 added findbest.py to contrib/ Skip Montanaro 2004-01-15 loosecksum.py: allow multiple mailboxes on the command line, not just a single message on stdin or a file containing one message Skip Montanaro 2004-01-15 Add -o option to allow users to set arbitrary global options from the command line to dbexpimp, imapfilter, mboxtrain, notesfilter, pop3dnd, server, upload, xmlrpcserver. Tony Meyer 2004-01-13 Fix [ 874784 ] Error in onReview code Skip Montanaro 2004-01-13 UserInterface: Split digest auth info properly. Simple split-on-comma fails if the uri contains commas. Barry A. Warsaw 2004-01-13 imapfilter: Catch and ignore MessageParseErrors when parsing the data['RFC822'] text. Tony Meyer 2004-01-12 Improve the messageinfo database so that more than two attributes can be saved (it's still backwards compatible with the old type). Tony Meyer 2004-01-12 Path/file options are no longer relative to the current working directory, they are relative to the last configuration file loaded. Tony Meyer 2004-01-12 pop3dnd: Store the IMAP flags. Tony Meyer 2004-01-12 pop3dnd: Fix an incorrectly checked flag (typo). Anthony Baxter 2004-01-12 New options ham_discard_level and spam_discard_level. These make the interface default to discard hams/spams in the training interface. Tony Meyer 2004-01-11 mkgraph.py: Add a docstring. Tony Meyer 2004-01-11 mkgraph.py: Add -f command line arg to pass a filename rather than reading from stdin (which is still the default) Tony Meyer 2004-01-11 mkgraph.py: Add a training_is_ham line to the error graph which shows the percentage of training data that is ham (i.e. shows the imbalance). Tony Meyer 2004-01-11 mkgraph.py: Modify the outputing so that it can be in different formats for those of us without plotmtv. The -c command line option outputs all the lines in the same set of rows, rather than in their own set as is the default. The -s arg specifies the separator for this sort of output (defaults to a comma, so that csv files are output). Tony Meyer 2004-01-11 incremental.py: Add a docstring and the ability to print it with -h or --help to incremental.py Tony Meyer 2004-01-11 regimes.py: Add a docstring that outlines the various regimes in hopefully easy to understand terms. Print this out if regimes.py is executed. Tony Meyer 2004-01-11 regimes.py: Add a new regime - balanced_corrected. Tony Meyer 2004-01-08 Fix [ 805852 ] need python-dev package on Debian Skip Montanaro 2004-01-08 table.py: space the table out a little more. Skip Montanaro 2004-01-07 mkreversemap.py: New script which generates a pickle file mapping features to mailbox files and message-id's. Use with extractmessages.py. Skip Montanaro 2004-01-07 extractmessages.py: New script; use with mkreversemap.py to identify messages in your training database which contain interesting tokens. Tony Meyer 2004-01-07 Fix [ 872044 ] HTTP review page date problems. Skip Montanaro 2004-01-06 Add experimental option and code to pick out some semantic bits from URLs Tony Meyer 2004-01-05 Add extra utility functions to oe_mailbox for dealing with Outlook Express. Tony Meyer 2004-01-05 Have autoconfigure confirm that configuration has occured. Tony Meyer 2004-01-05 Do better 'is installed' checks in autoconfigure. Adam Walker 2004-01-05 Start SMTP proxy in a trainable state. Tony Meyer 2003-01-02 UserInterface: Fix import error reported. Richie Hindle 2004-01-01 Default to twenty search results on web interface rather than just one. Richie Hindle 2004-01-01 Made the search form do a GET rather than a POST. Richie Hindle 2004-01-01 Fix for 842984: If webbrowser.open_new() fails, print a message saying "Please point your web browser at http://localhost:8880/" rather than bombing out. Richie Hindle 2004-01-01 New script: utilities/hammer.py: Hammers the core SpamBayes code, repeatedly training and classifying using faked-up messages. Tony Meyer 2003-12-31 pop3dnd: Fix fetching an envelope. Tony Meyer 2003-12-31 pop3dnd: Handle storing no flags. Tony Meyer 2003-12-31 pop3dnd: Update the RETR'ing of messages to reflect what sb_server currently does. Tony Meyer 2003-12-31 pop3dnd: Clean out some cruft that isn't necessary with the latest version of twisted (1.1) Tony Meyer 2003-12-31 pop3dnd: Add two new Message classes, one for messages that are stored in memory, and one for messages that are re-generated each time the message is loaded. Tony Meyer 2003-12-31 pop3dnd: Start our UIDs at 1, not 0, because Eudora likes this more. Tony Meyer 2003-12-31 pop3dnd: Don't override the imported name "message". Tony Meyer 2003-12-31 pop3dnd: Add a new folder - INBOX - this holds any messages from SpamBayes to the user. (Having INBOX as an alias for Spam wasn't working well, and being able to communicate within the confines of the mailer is nice, too). Tony Meyer 2003-12-31 pop3dnd: Don't let the user set so much via the command line. Use a config file, you lazy person. Tony Meyer 2003-12-31 pop3proxy_tray: When we stopped sb_server and then restarted, we didn't init the state, so it wouldn't work. Fix that. Tony Meyer 2003-12-31 Web interface: As Richie pointed out, the status message was only updated when the state was recreated. Fix this. Tony Meyer 2003-12-31 Web interface: Output plurals correctly in stats information. Tony Meyer 2003-12-31 We printed out false positive numbers in the false negatives section, and vice versa. Fix. Tony Meyer 2003-12-30 IMAP interface: Quote folder names when displaying them - otherwise if the folder names contained certain characters it could result in bad html (if the name was ">foo", for example). Tony Meyer 2003-12-29 Web interface: Improve the 'online' help message for the review page, and add messages for the stats and home pages. Tim Peters 2003-12-29 Many improvements to the mksets.py testtools script. Tim Peters 2003-12-28 sort+group.py: Sort msgs by full-precision timestamp (not just by day). Normalize Received time to UTC first. Use email.Utils to parse dates instead of hand-rolling our own parser Tim Peters 2003-12-28 sort+group.py: Preserve files' extensions (if any) during renaming. Tim Peters 2003-12-28 Outlook: export.py - the -n option now gives the number of Set subdirectories desired, instead of a number of msgs per Set subdir "to shoot for". Tim Peters 2003-12-28 Added a new -t option to rebal.py, may have broken -s and -r options. Tim Peters 2003-12-26 Many improvements to the rebal.py testtools script. Tim Peters 2003-12-26 Many improvements to the export.py script for Outlook. Skip Montanaro 2003-12-24 storage: make state key a manifest constant Mark Hammond 2003-12-23 Along with checking Outlook isn't running, check Outlook's mail transport also isn't running, and that an existing sb_server isn't. Mark Hammond 2003-12-23 Fix uninstall problem - uninstall should be 100% clean, assuming Outlook isn't running. Mark Hammond 2003-12-23 Tray app: Binary version failed to check for most recent version. Skip Montanaro 2003-12-23 Sendmail annotates the Received: header with "(may be forged)" if it thinks the sender is forging its identity. Generate a token for this, if we are mining received headers. Tony Meyer 2003-12-22 Move OE specific stuff out from UserInterface.py to oe_mailbox.py. Mark Hammond 2003-12-22 Outlook: Default to background filtering being on for new versions. Tony Meyer 2003-12-22 Web interface: A beginning at a more userfriendly interface to the testing setup. Mark Hammond 2003-12-22 Outlook: When a frozen executable, addin.py becomes a mini-installer EXE for the DLL. Mark Hammond 2003-12-21 Outlook: DWhen doing a "batch train" (eg, selecting multiple messages and saying "Delete as" or "Recover from",) the DB was saved in between each and every message. Now only saved at the end (which was always the intent) Mark Hammond 2003-12-21 Outlook: DAs part of checking our configuration is invalid, make sure the user hasn't set us up such that either Spam/Unsure folders isn't also being watched for new messages Mark Hammond 2003-12-21 Outlook: If the user attempts to close the Manager dialog while there is a problem preventing us being enabled, confirm they really want to close it Mark Hammond 2003-12-20 dump_props.py: Add -c option, which writes output to the Windows clipboard. Mark Hammond 2003-12-20 Outlook: Include the foldername in many messages, so help track down wierd bugs from user logs. Say what we are watching a folder for. Mark Hammond 2003-12-20 Outlook: Fix [ 860410 ] SpamBayes allows top-level folders to be selected, and also prevent a single-select dialog from closing with no selection Skip Montanaro 2003-12-20 Tokenizer: Solved the "backwards breakdown" problem with ip addresses in Received: headers. Skip Montanaro 2003-12-20 Tokenizer: Tightened up recognition of hostnames and accepted bracketed or parenthesized ip addresses without requiring a leading space. Mark Hammond 2003-12-19 Outlook: Remove handling of E_OBJECT_CHANGED exception, as it simply did not work. Mark Hammond 2003-12-19 Fix [ 803798 ] MAPI_E_OBJECT_CHANGED error saving spam score, which is a dupe of [787676], which was incorrectly marked as fixed Mark Hammond 2003-12-19 Outlook: Don't record in the training database unless we are successful in the filter - otherwise future attempts to filter will get all screwed up, as it will think it already was Mark Hammond 2003-12-19 Outlook: Move some of our init code from OnConnection to OnStartupComplete Mark Hammond 2003-12-19 Outlook: Try and tone down the toolbar message in the log to prevent people reporting it as a bug Mark Hammond 2003-12-19 Outlook: Handle situations where Outlook starts up in a confused state, which then confused us. Mark Hammond 2003-12-19 Outlook: Ask if you want the slow, non-filter tests run, and add E_OBJECT_CHANGED tests, as per [ 803798 ] MAPI_E_OBJECT_CHANGED error saving spam score Tony Meyer 2003-12-18 Bring pspam into the modern SpamBayes world. Mark Hammond 2003-12-18 Outlook: When the 'New Folder' button was used to create a folder, that folder was not used when you closed the dialog, even though it was selected. Mark Hammond 2003-12-17 Tray app: Better icons and icon loading code. Tony Meyer 2003-12-17 Add the basis of a new experimental (and highly debatable) option to 'slurp' URLs. Tim Peters 2003-12-17 Implemented the intended "tiling" version of x-use_bigrams. Tony Meyer 2003-12-16 Option names are always case insensitive, no matter what. Tony Meyer 2003-12-16 Fix a bug in the web interface where the probability would be incorrectly calculated on 'show clues'. Tony Meyer 2003-12-16 New experimental option: x-use_bigrams. Skip Montanaro 2003-12-16 mboxutils: This change generalizes the DirOfTxtFileMailbox class to assume all non-directory files contain a single message and to recursively descend into subdirectories of the argument directory. Tony Meyer 2003-12-15 Add a warning as a temporary solution for Python bug #845560. Tony Meyer 2003-12-15 Add the missing code for the Habeas headers tokenizing (and deprecate). Mark Hammond 2003-12-15 Fix [ 833439 ] default_bayes_customize.ini is confusing. Mark Hammond 2003-12-14 Move the option loading code to a function, then call this function as the module loads. Mark Hammond 2003-12-14 test_programs: When "calling" URLs, check the output for tracebacks, check the exit code of processes we spawn, and add test for "[ 859215 ] "Restore Defaults" causes assertion error at exit". Tim Peters 2003-12-14 Removed support code for the defunct experimental_ham_spam_imbalance_adjustment option Mark Hammond 2003-12-14 Fix [ 856628 ] reload(Options) fails in windows binary Mark Hammond 2003-12-14 Fix [ 859215 ] "Restore Defaults" causes assertion error at exit. Tony Meyer 2003-12-14 ImapUI: When logging in was done by the UI (to show available folders) we assigned the imap_session object to the wrong name Skip Montanaro 2003-12-10 Loosen constraints on HEADER_VALUE regular expression. Skip Montanaro 2003-12-10 Add ability for "x-" options (deprecated, or experimental). Mark Hammond 2003-12-10 Outlook: Try and add the Spam field to the 'Unsure' folder in the same way we do for the Spam and watch folders. Mark Hammond 2003-12-10 Fix [ 856141 ] Spam field not added to unsure or empty folders Mark Hammond 2003-12-08 Outlook: Add/Fix a number of 'unicode file' related comments. Mark Hammond 2003-12-08 Outlook: Allow multiple manager objects to work in the same process (but not at the same time): Mark Hammond 2003-12-08 Outlook: A number of changes to better support us existing in the 'COM Addins' list when running the binary version Tony Meyer 2003-12-04 Tray app: Change the default (double-click) behaviour of the tray to "review messages" rather than "display information". Tony Meyer 2003-12-04 Tray app: use SetDefaultItem (so the default action is in bold in the menu). Mark Hammond 2003-12-03 For the unittest scritps avoid sys.path munging. Mark Hammond 2003-12-03 Add new test_programs unittest script and support file for unittest scripts. Mark Hammond 2003-12-02 sb_server was ignoring command-line options; fix. Richie Hindle 2003-11-27 Sjoerd's improved version of patch 831388. Neale Pickett 2003-11-27 sb_filter now prints each message only once, not once per argument :) Tony Meyer 2003-11-26 sb_dbexpimp.py: Import/Export data as utf-8. Richie Hindle 2003-11-26 UserInterface.py More robust code for parsing score headers - copes with the presence of logarithms. Richie Hindle 2003-11-26 UserInterface.py: More robust code for parsing evidence headers. Copes with ';' and ': ' being part of a clue. Richie Hindle 2003-11-26 Patch [ 831388 ]: Make message.py respect the header_score_digits option. Richie Hindle 2003-11-26 Made sb_filter obey the notate_to and notate_subject options. Tony Meyer 2003-11-26 As we now use whichdb to figure out what type of file the db is, if we were using windows and python 2.2 we would try and use dbhash instead of db3hash, which is a Bad Thing. Fix this. Tony Meyer 2003-11-26 message.py: Encode words in the evidence header as utf-8 if they are unicode. Barry A. Warsaw 2003-11-25 sb_xmlrpcserver.py: Make sure that the socket being bound is reusable. Barry A. Warsaw 2003-11-25 Change XMLHammie.score() so that the float score is returned directly instead of trying to be wrapped in a Binary object. Barry A. Warsaw 2003-11-25 New script: sb_evoscore.py - A shim script between sb_xmlrpcserver.py and Ximian Evolution. Skip Montanaro 2003-11-25 Added a makefile to the testtools directory to make using timcv easier. Neale Pickett 2003-11-18 Cleanup sb_filter and sb_mboxtrain. Richie Hindle 2003-11-16 Patch [ 842464 ] Correct installation instructions from "setup.py install" to "python setup.py install" Skip Montanaro 2003-11-13 sb_filter: add -o/--option command line arg that allows user to set any options value from the command line Skip Montanaro 2003-11-13 OptionsClass: Add set_from_cmdline() Skip Montanaro 2003-11-13 sb_filter: Allow multiple types of mailboxes to be processed using mboxutils.getmbox. If any mailbox files are given on the command line, the output is always a Unix-style mailbox containing From_ lines. Richie Hindle 2003-11-11 notesfilter: The header_x_string options now live in the Headers section, not the Hammie section. Richie Hindle 2003-11-07 Fixed an infinite loop when you break the browser connection to sb_server when sb_server is busy training. Anthony Baxter 2003-11-05 Spell-checked all the HTML and txt files Tony Meyer 2003-10-30 Implement [ 827138 ] Can't display clues/tokens/source for a trained message Richie Hindle 2003-10-15 Increased the auth-digest login timeout from one minute to twenty. Neale Pickett 2003-10-15 Modified muttrc and spambayes.el that actually work with what's being shipped :) Neale Pickett 2003-10-15 Expanded documentation of sb_filter.py Skip Montanaro 2003-10-15 which_database: fix bug in dbhash/bsddb[3] distinction and avoid overriding str Skip Montanaro 2003-10-15 which_database: need to call os.path.expanduser() since paths like ~/hammie.db are valid in the options file Mark Hammond 2003-10-10 It is no longer necessary to pre-load our default message store, and doing so caused us to fail if this default store was in offline mode. Tony Meyer 2003-10-08 When training via the web interface record the training in the messageinfo db. Tony Meyer 2003-10-07 Fix [ 818871 ] sb_server.py calls undefined variable Tony Meyer 2003-10-06 Add Tim's fix for the stats to the web interface stats as well. i.e. round the percentages, don't truncate them. Tony Meyer 2003-10-06 imapfilter: When we mark a message as deleted, mark it as read (seen) as well. Richie Hindle 2003-10-04 Fix the help icon to look like the rest. Tony Meyer 2003-10-03 If sb_imapfilter.py is run without any switches, just serve the web interface (but don't launch a browser). Tim Peters 2003-10-03 Outlook: Stop spam and unsure messages being double-counted in the stats. Tony Meyer 2003-10-02 Fix a bug where messages wouldn't be trained via the web interface although no error would be reported (introduced after the previous release). Tony Meyer 2003-10-02 Provide a partially filled-in bug report message via the web interface. Tim Peters 2003-10-01 Round (not truncate) the stats information in the Outlook plugin. Tony Meyer 2003-09-30 Improve autoconfigure script to find the location of various config files. Tony Meyer 2003-09-30 Add basic statistics information to the web interface. Tony Meyer 2003-09-29 Add a basic help system to the web interface. Tony Meyer 2003-09-29 Add warning information to the web interface, for example if the user has imbalanced training, or not much training. Mark Hammond 2003-09-29 Don't start the pop3proxy service if a proxy is already running. Mark Hammond 2003-09-29 Outlook: Add slightly better stats, and a better framework to extend. Skip Montanaro 2003-09-26 Dump TRAINED_HDR global. Reference options[...] instead. Mark Hammond 2003-09-25 Add patch [ 809008 ] safe start/stop and exclusive execution on windows Tony Meyer 2003-09-24 sb_filter.py: If the -n switch was before the -d/-p switch, then the name wouldn't be used. This is rather unintuitive, so fix this so that the -d/-p name is used wherever the -n switch is. Adam Walker 2003-09-24 pop3proxy_tray: Check if the web interface port can be bound as a simple test of if the proxy is running. Tony Meyer 2003-09-20 smtpproxy is now only a module, not a script. Use sb_server instead. Tony Meyer 2003-09-20 Consolidate some of the many message classes - in particular Corpus.Message is removed in favour of message.Message Tony Meyer 2003-09-20 Improve the 'Find Message' query on the front page of the web UI. Tony Meyer 2003-09-20 Add an advanced word query to the web UI. Tony Meyer 2003-09-20 Make the review messages page on the web UI more customizable. Release 1.0 Alpha 8 =================== There was no Alpha Release 8. Release 1.0 Alpha 7 =================== Skip Montanaro 2003-10-28 Determine dbm format from the file if it already exists Tony Meyer 2003-10-09 An old-style option was left in hammiebulk; fix this. Tony Meyer 2003-10-08 If the (stats) db uses a pickle, then use a pickle for the messageinfo db as well. Tony Meyer 2003-10-08 Try and close the db when we are no longer using it. Tony Meyer 2003-10-08 Ensure that we store the messageinfo database when changes are made. Tony Meyer 2003-10-07 Fix [ 818552 ] Exchange 2000 IMAPserver & imaplib.error: APPEND command er Tony Meyer 2003-10-06 Trivial fix for IMAP over SSL being offered when it is available. Tony Meyer 2003-10-03 Fix [ 810342 ] sb_smtpproxy does not work with smtplib.SMTP.sendmail() Tony Meyer 2003-10-03 Fix [ spambayes-Bugs-816400 ] Crash because of bad date. Sjoerd Mullender 2003-10-02 imapfilter: if problems occur parsing the date, just use the current date/time. Tony Meyer 2003-09-30 Fix [ 814322 ] AttributeError: hdrtxt Tony Meyer 2003-09-29 smtpproxy: If we successfully trained a message from the pop3proxy cache or IMAP server, we still said that we couldn't find it. Fix. Tony Meyer 2003-09-29 smtpproxy: Fix a minor printing error that would cause a traceback. Tony Meyer 2003-09-29 imapfilter: Fix trying to view IMAP folders before restarting and after entering in details. Tony Meyer 2003-09-28 Don't use 'pragma: no_cache' to try and stop browsers caching the web interface pages. Skip Montanaro 2003-09-26 Correct sense of include_trained test in mbox_train. Tony Meyer 2003-09-26 If the user is using Windows, also install pop3proxy_service.py and pop3proxy_tray.py. Tony Meyer 2003-09-24 Fix sb_xmlrpcserver to work with the renamed options/scripts. Tony Meyer 2003-09-24 Fix [ 809769 ] TypeError when training 1.0a6 Release 1.0 Alpha 6 =================== Skip Montanaro 2003-09-19 Worm around a possible email pkg bug. Skip Montanaro 2003-09-19 hammiebulk & mboxtrain: Minor performance boost when training on lots of mail. Skip Montanaro 2003-09-19 Place a threshold on the number of items displayed per section in the review page of the UI. Tony Meyer 2003-09-18 Change the Outlook plug-in to use the general default (currently False) for the experimental_ham_spam_imbalance adjustment. Tony Meyer 2003-09-18 Change the default for address_headers to include to, cc, reply-to, and sender as per Tim's suggestion. Tony Meyer 2003-09-18 Store the user's caches and messageinfo.db file in the app data folder as well (by default), if we have everything else there. Tony Meyer 2003-09-18 If we can't find a config file anywhere but the windows app data directory, then load it. Tony Meyer 2003-09-18 Use urllib not urllib2 to shut down the proxy. Tony Meyer 2003-09-18 Create the server strings for the UI *after* reading in the command line parameters. Tony Meyer 2003-09-18 Move notate_to and notate_subject options to the "Headers" section. Tony Meyer 2003-09-18 Move all the storage options in the "pop3proxy" section to the "Storage" section. Tony Meyer 2003-09-18 Remove the hammie debug options, and use the "Headers" evidence options instead. Tony Meyer 2003-09-17 Fix [ spambayes-Bugs-806632 ] sb_server failure when saving config Tony Meyer 2003-09-17 Outlook: Add a warning for those with highly (>5 times) imbalanced ham and spam. Mark Hammond 2003-09-17 pop3proxy_service: Only munge sys.path in source-code versions, and as the service starts, have it report the username and ini file it is using. In binary builds, write a log to %temp%\SpamBayesServicen.log Mark Hammond 2003-09-17 If we are running Windows, have no valid config file specified, and have the win32all extensions available, default to: \Documents and Settings\[user]\Application Data\SpamBayes\Proxy Tony Meyer 2003-09-16 Fix [ 795145 ] pop3proxy review page dies with mixed gzip/non messages Mark Hammond 2003-09-15 Outlook: Load dialog bitmaps directly from the executable in binary builds. Adam Walker 2003-09-15 Updated pop3proxy_tray to support the service. Tony Meyer 2003-09-15 Removed the gary_combining option and code. Tony Meyer 2003-09-15 Add a (basic) check for version option to the pop3proxy tray app. Tim Peters 2003-09-15 Fix SF bug 806238: urllib2 fails in Outlook new-version chk. Tim Peters 2003-09-15 Outlook: ShowClues(): Made the clue report a little prettier, and (I hope) a little easier to follow. Tony Meyer 2003-09-13 Fix [ 805351 ] If cc: address is not qualified, proxy fails to send message Mark Hammond 2003-09-12 pop3proxy_tray: Use simple logging strategy similar to the Outlook addin - if we have no console, redirect either to win32traceutil (source-code version) or to a %TEMP\SpamBayesServer1.log (yet to be released binary version). Mark Hammond 2003-09-12 pop3proxy_tray: When running from binary, don't hack sys.path. When running from source code, hack sys.path based file path rather than on os.getcwd. Mark Hammond 2003-09-12 pop3proxy_tray: When running from binary, load the icon from the executable rather than a .ico file. Tim Peters 2003-09-12 Outlook: ShowClues(): Add lines revealing the # ham and spam trained on. Tony Meyer 2003-09-11 When running setup.py if the old (named) files exist, offer to delete them. Tony Meyer 2003-09-11 Add a unittest to check that we correctly fail when no dbm modules are available. Anthony Baxter 2003-09-11 Add a new file: NEWTRICKS.TXT to record ideas that have and haven't been tried. Richie Hindle 2003-09-11 Bug 803501: Fix the "No dbm modules available" message to print rather than crash. Skip Montanaro 2003-09-11 Implement a better fix for the storage.py pickle/dbm problems. Mark Hammond 2003-09-10 Outlook: use the classifier's (new) store method rather than an Outlook specific one. Tony Meyer 2003-09-10 Re-fix storage.py so that hammie works with a pickle or dbm. Tony Meyer 2003-09-09 Fix for [ 802545 ] crash when logging off imapfilter UI Tony Meyer 2003-09-09 Fix for [ 802347 ] multiline options saved incorrectly Tony Meyer 2003-09-09 Implement half of [ 801699 ] migration from hammie to sb_* scripts (although in a different way) Tony Meyer 2003-09-09 sb_server should store and close the db before reopening it. gdbm fails if we do not do this. Fix this. Tony Meyer 2003-09-09 imapfilter: correctly handle IMAP servers that (wrongly) fail to put folder names in quotation marks Mark Hammond 2003-09-09 Add a close method to the various storage classes. Sjoerd Mullender 2003-09-08 Fix for [ 801952 ] Imapfilter appending headers Sjoerd Mullender 2003-09-08 imapfilter: Count all messages being classified instead of just the ones from the last folder. Sjoerd Mullender 2003-09-08 Trivial fix for IMAP over SSL. Tony Meyer 2003-09-08 imapfilter: handle a folder name as a literal when presenting a list to choose from Tony Meyer 2003-09-08 imapfilter: handle IMAP servers that don't pass a blank result line for an empty search Mark Hammond 2003-09-08 Outlook: When we fail to add the 'Spam' field to a read-only store (eg, hotmail), complain less loudly. Mark Hammond 2003-09-08 Fix [ 798362 ] Toolbar becomes unresponsive and must be recreated Tony Meyer 2003-09-06 Add a missing file that's necessary to try out the sb_pop3dnd.py script, and the version and options necessary. Tony Meyer 2003-09-06 Fix [ 800555 ] 1.0a5 release missing key outlook files. Tony Meyer 2003-09-05 Remove backwards compat code for options, and update all (I hope) the remaining code that uses it. Tony Meyer 2003-09-05 Move and rename all top-level scripts other than setup.py Release 1.0 Alpha 5 =================== Tony Meyer 2003-09-03 imapfilter: We would crash if we hadn't set anything up and didn't prompt for password. Adam Walker 2003-09-03 pop3proxy_tray: Switch icons based on if the proxy is running or not. Provide some info found on the information page in the tooltip of the icon. Tony Meyer 2003-09-02 Add a rough autoconfigure script, which will set up both spambayes and a mail client to use pop3proxy Richie Hindle 2003-09-01 Fix [ 797890 ] "assert hamcount <= nham" problem. Tony Meyer 2003-09-01 Fix [ spambayes-Bugs-796996 ] SMTP server not started until restart. Richie Hindle 2003-09-01 Integrated [ 791393 ] HTTP Authentication, which closes #791319. Tony Meyer 2003-09-01 Added [ 796832 ] Word query should show all words starting with certain text Tony Meyer 2003-09-01 Fix for [ 797316 ] Extra CRLF to SMTP server causes garbage error. Richie Hindle 2003-08-31 The web UI's Shutdown command, and stopping the pop3proxy_service, now wait for any open proxy connections to finish before exiting the process. Richie Hindle 2003-08-31 X-Spambayes-Exception headers now contain a traceback as well as the exception. Richie Hindle 2003-08-29 Fix the yellow colour of the header boxes in the web interface in strict browsers (i.e. not IE ;) Tony Meyer 2003-08-28 smtpproxy: try a bit harder to proxy the exact command if we aren't going anything with it, and try to match it more closely even if we are. Mark Hammond 2003-08-27 Outlook: all menu sub-items are now temporary. Mark Hammond 2003-08-27 Fix [ 776808 ] Expanding toolbar crashes outlook Mark Hammond 2003-08-27 Outlook: ensure we only try and use tree items listed as valid by the mask. Mark Hammond 2003-08-27 Fix [ 795749 ] "Score after training" doesn't in CVS Mark Hammond 2003-08-27 Outlook: add 'View Log' button to the Diagnostics dialog. Tony Meyer 2003-08-26 Fix restoring defaults from the web interface. Tony Meyer 2003-08-26 Implement [ 791254 ] Advanced configuration panel. Tony Meyer 2003-08-26 smtpproxy: don't convert unknown commands to upper case as this mucks about with passwords, which might be case sensitive. Richie Hindle 2003-08-26 Fixed [ 787251 ] Problem refreshing message list and [ 790051 ] Can't review messages. Richie Hindle 2003-08-26 Added a missing line break in the status pane on the Home page when there are no proxies configured. Richie Hindle 2003-08-26 Fix [ 743131 ] Specifying a POP3 server on the pop3proxy command line but no local port number (ie. no -l) now works again, defaulting to local port 110. Skip Montanaro 2003-08-26 Change the storage.py print statements to print to stderr, so that using verbose=True is possible using apps that work through stdin. Mark Hammond 2003-08-26 Fix [ 779319 ] ntpath Unicode error Mark Hammond 2003-08-26 Outlook: default folder names used by the Wizard are now "Junk E-Mail" and "Junk Suspects" Mark Hammond 2003-08-26 Outlook: the experimental 'timers' options got upgraded to the 'filter' section. Adam Walker 2003-08-26 Outlook: added button on advanced tab to display the spambayes data folder. Adam Walker 2003-08-26 Outlook: move Filter Now to a separate dialog invoked by the drop down menu on the toolbar. Tony Meyer 2003-08-25 Fix some old option names. Tony Meyer 2003-08-25 Change the notate_to and notate_subject options to notate for spam, unsure, ham, or any combination (including none) thereof. Tony Meyer 2003-08-25 Add no_cache_large_messages option. If messages are bigger than this, don't cache them (to avoid caching messages with massive attachments that are already correctly classified). Tony Meyer 2003-08-25 Add an open_storage function to centralise opening storage, which also works with the new SQL classifiers. Used by pop3proxy and hammiefilter at the moment. Tony Meyer 2003-08-25 smtpproxy should now work as previously, but also training on the exact message sent, rather than looking up via id. Lookup into the imapfilter folders is also possible. Mark Hammond 2003-08-25 Fix [ 785389 ] Folders missing in Spambayes manager. Mark Hammond 2003-08-25 Fix [ 786952 ] Error when profile name has invalid filename characters. Mark Hammond 2003-08-25 Outlook: We now work with "IPM.Note" and "IPM.Anti-Virus*" Mark Hammond 2003-08-25 Outlook: Fail when we can't find a default message store. Mark Hammond 2003-08-25 Fix "[ 788495 ] Filter fails when folder full" Mark Hammond 2003-08-25 Outlook: remove last win32ui dialog. Mark Hammond 2003-08-25 Outlook: change default filter action to "move" Mark Hammond 2003-08-25 Outlook: don't score if the training was canceled. Mark Hammond 2003-08-25 Outlook: present the wizard when required. Mark Hammond 2003-08-25 Outlook: allow filtering to be enabled, even if no training information! Tony Meyer 2003-08-23 Add [ 789916 ]. Outlook Express .dbx files can now be used to train, just like mbox files. Mark Hammond 2003-08-23 Outlook: we can cancel a train without destroying the DB, and mail that comes in during the training will not use the partial db. Mark Hammond 2003-08-23 An empty 'allowed values' now allows an empty string. Mark Hammond 2003-08-23 Outlook: add a "New Folder" button to the folder selector dialog. Mark Hammond 2003-08-20 Outlook: add wizard dialogs. Tony Meyer 2003-08-19 Implement [ 790615 ] Allowed remote connections management, which addresses [ 698036 ] pop3proxy security Tony Meyer 2003-08-19 Allow @ and = in paths. Adam Walker 2003-08-19 Outlook: add a Wizard framework. Adam Walker 2003-08-19 Outlook: add a diagnostic dialog. Adam Walker 2003-08-18 Fix [ 790406 ] Allow the timers to be set in half second increments. Mark Hammond 2003-08-18 Improve the starting/stopping of the pop3proxy service Adam Walker 2003-08-18 Outlook: change the manager dialog to a tabbed interface, and add a logo to it. Tony Meyer 2003-08-18 Allow the pop3proxy_service to start smtpproxy as well Tony Meyer 2003-08-18 Add version information to the web interface, as requested in [ spambayes-Bugs-790051 ] Can't review messages if probability header is turned on Tony Meyer 2003-08-16 which_database.py: Make the script a little smarter about checking if bsddb[3] is available. Tony Meyer 2003-08-14 Fix [ spambayes-Bugs-788008 ] smtpproxy.py assumes too good format for addresses Tony Meyer 2003-08-14 Implement patches from [ 788001 ] mboxtrain.py Maildir bugfix and feature Tony Meyer 2003-08-14 Fix [ 787296 ] Local installation problem: hammiefilter_persistent_storage Tony Meyer 2003-08-14 Fix [ 788002 ] hammiebulk.py uses wrong option name Tony Meyer 2003-08-13 If you didn't use the -p switch to enter your password interactively, imapfilter would try and get it from the options, but if it wasn't there yet (because you hadn't done the setup yet), it would crash. Fix this. Skip Montanaro 2003-08-13 Add a simple n-way classifier using a cascade of binary SpamBayes classifiers Tony Meyer 2003-08-12 Web interface: At some point (before the 1.0a4 release) selecting both "header" and "body" stopped working. This fixes that. Tony Meyer 2003-08-12 Have the review messages page put the unsure messages at the top, because they are the most important to take action on Mark Hammond 2003-08-11 Outlook: SpamClues shows the percentage as well as the raw score. Adam Walker 2003-08-09 Outlook: Added website links to the help menu. Adam Walker 2003-08-09 Use the Tahoma font in the Outlook dialogs. Mark Hammond 2003-08-09 Fix [ 780612 ] Outlook incorrectly trains on moves messages. Mark Hammond 2003-08-08 Fix bug [ 784323 ] Plug-in will not initialize/watch in offline mode Tony Meyer 2003-08-07 Add a mySQLClassifier class Skip Montanaro 2003-08-07 Add a postgreSQL classifier class, and a base SQLClassifier class Tony Meyer 2003-08-07 Remove dumbdbm from the config options Skip Montanaro 2003-08-07 Remove dumbdbm from the possible dbm storage options Tony Meyer 2003-08-07 Fix [ 784296 ] imapfilter broken with Python 2.3 Adam Walker 2003-08-06 Outlook: move the html menu options to a Help sub-menu. Mark Hammond 2003-08-06 Fix [ 780819 ] Images pasted to clipboard each startup Mark Hammond 2003-08-04 Add a get_option method, so an option instance itself can be fetched. Mark Hammond 2003-08-04 Outlook: new data driven dialogs loaded from Windows .rc scripts. Mark Hammond 2003-08-04 Outlook: add a resource script parser, by Adam Walker. Mark Hammond 2003-08-01 Outlook: fix [ 780801 ] IMAP Still Failing - GetField() returns None on MAPI error Mark Hammond 2003-07-30 Outlook: avoid passing a float to C functions that take an int (the slider pos) Mark Hammond 2003-07-29 Fix [ 779049 ] email.Errors.HeaderParseError: Continuation line seen ... Mark Hammond 2003-07-29 Outlook: change the way we detect 'unsent' items - this way catches both unsent items, and copies of sent items. Mark Hammond 2003-07-29 Outlook: add ability to check for the latest version. Mark Hammond 2003-07-29 Version.py: indicate the generated output is generated :) Make work for Python 2.2, but don't trust its config parser for remote data. Mark Hammond 2003-07-29 Support fetching the "latest" set of version data from the spambayes web site. Richie Hindle 2003-07-28 Made the pop3proxy work with fetchmail. Mark Hammond 2003-07-28 Outlook: Add a new experimental 'timer'. Mark Hammond 2003-07-26 Outlook: Log the binary version in the binary. Mark Hammond 2003-07-25 Fix locale issues in Outlook - fixes [ 765912 ] AssertionError: Proportions must add to 1.0 ... Tim Peters 2003-07-25 Fix SF bug 777026: Possible cause for db corruption in DBDictClassifier. Mark Hammond 2003-07-25 Created a directory & readme for test suites, including a storage.py test. Tony Meyer 2003-07-25 [ 777165 ] Typo in Options.py causes bogus warning on reading config Mark Hammond 2003-07-23 Outlook: fix a bug in how attachment properties were fetched - if the body was "large", we attempted to get the "large property" from the mail object itself, rather than the attachment. Mark Hammond 2003-07-23 Outlook: fix a problem where multipart/signed messages could still screw us. Mark Hammond 2003-07-23 Fix [ 693387 ] user-composed messages are filtered. Mark Hammond 2003-07-23 Outlook: Check the message flag for the "unsent" bit. Richie Hindle 2003-07-22 You can once again specify local addresses as well as ports for the pop3proxy to listen on (was broken in 1.0a3 and 1.0a4). Mark Hammond 2003-07-21 Outlook: Paul Moore reports that not specifying USER_DEFERRED_ERRORS solves his "unread flag" issue - and we certainly don't need that flag here, so out it goes. Mark Hammond 2003-07-21 Outlook: While we are printing versions, Python gets a go. Mark Hammond 2003-07-21 Fix 690418: Non mail items filtered by Outlook Mark Hammond 2003-07-21 Fix 719586: Cannot View Spam Cues for Undeliverable Reports Mark Hammond 2003-07-21 Outlook: Add IsFilterCandidate() method to a message object to determine if this is a message we should try and filter. Mark Hammond 2003-07-21 Outlook: Rationalize code that creates a message object. Mark Hammond 2003-07-21 Outlook: Ignore errors when enumerating stores. Mark Hammond 2003-07-21 Outlook: Work better with a unicode data directory. Mark Hammond 2003-07-21 Outlook: Add a new filter option - "save_spam_info", default=True. Mark Hammond 2003-07-20 Fix [ 769346 ] Problems after deleting certain spam folder as implemented in [ 769981 ] Outlook plugin: allow user to change spam and unsure. Mark Hammond 2003-07-20 pop3proxy_service: log exceptions if the server thread dies unexpectedly. Mark Hammond 2003-07-20 Fix [ 761499 ] pop3proxy_service doesn't stop when shutdown from browser. Mark Hammond 2003-07-20 Fix [ 769346 ] Problems after deleting certain spam folder as implemented in [ 769981 ] Outlook plugin: allow user to change spam and unsure. Mark Hammond 2003-07-19 pop3proxy_service: Redirect output to win32traceutil (for want of a better place) Richie Hindle 2003-07-19 pop3proxy: Print a traceback as well as adding an X-Spambayes-Exception header when there's an exception raised while processing a message. Tony Meyer 2003-07-18 Fix [ 773452 ] Unable to use fractional number as spam_threshold. Richie Hindle 2003-07-18 pop3proxy: "ASCII decoding error" problem fixed. Skip Montanaro 2003-07-15 dbmstorage: trivial hack to give dumbdbm a sync() method (which Shelve will call) and hopefully reduce database corruption Mark Hammond 2003-07-09 Outlook: Include the Windows version in the log. Mark Hammond 2003-07-09 Outlook: Use the passed field name rather than hardcoded "Spam". Mark Hammond 2003-07-09 Fix 765042: IMAP mail fails to filter Tony Meyer 2003-07-09 Allow $ in pathnames, which fixes [ 768162 ] UNC path for data_directory? Tony Meyer 2003-07-09 mboxtrain: Use /tmp/ as the temp directory, not /cur/tmp or /new/tmp. Fixes [ 768221 ] v1.0a4 dies when training on Maildir Mark Hammond 2003-07-08 Fix [ 760062 ] Traceback untraining a single message - typo in assert. Tony Meyer 2003-07-08 hammiebulk: a belated patch to account for the default persistent_storage_file option changing. Richie Hindle 2003-07-07 Added no_cache_bulk_ham option: suppresses caching of 'Precedence: bulk' ham, to stop mailing list traffic swamping the web UI's training page. Richie Hindle 2003-07-07 Prevent the "Show clues" links on the web UI's training page from word-wrapping and making all the table rows two lines high. Release 1.0 Alpha 4 =================== Tony Meyer 2003-07-04 Fix SF#761677 ("mboxtrain.py's -n option has no effect") Richie Hindle 2003-07-03 Put the current date and time into the footer of the web interface, rather than always displaying "Mon Dec 30 14:04:32 2002". Richie Hindle 2003-07-03 Fix a bug in pop3proxy where long attachments would be broken. Richie Hindle 2003-07-02 If an exception occurs parsing a message in pop3proxy, append an 'exception' header to the message and recover. Richie Hindle 2003-07-02 Stop the pop3proxy including the trailing . when passing messages to the email package. Mark Hammond 2003-07-01 Version 003 of the binary. Mark Hammond 2003-07-01 In outlook plugin, display a message for "Delete as Spam" or "Recover from Spam" when SpamBayes is not enabled. Skip Montanaro 2003-07-01 Encode unicode objects as utf-8 before using as a key for DBDictClassifier instances. (Fixes [ spambayes-Bugs-761670 ] Unexpected unicode key inbsd db) Mark Hammond 2003-07-01 In Outlook plugin, toolbar was not initialized when "Outlook Today" was the default view. Mark Hammond 2003-06-30 In Outlook plugin, don't (try to) do OnStartupComplete work if OnConnection failed. Mark Hammond 2003-06-30 In Outlook plugin, log the repr() of messages displayed in a dialog, as they often have embedded \r\n chars - repr() keeps the whole thing to a single line. Tim Peters 2003-06-28 A new stripper to squash yet another way of hiding content in HTML spam, like Ereywl55ctions to hide Erections. Tim Peters 2003-06-27 In storage.py store(): If a Shelf db doesn't have a key, then "del db[key]" should raise KeyError. Tim Peters 2003-06-27 In storage.py store(): Use iteritems() instead of items() to materialize the changed_words guts. Tim Peters 2003-06-27 In storage.py, check WORD_CHANGED and WORD_DELETED with is not ==. Tim Peters 2003-06-27 Remove a superstitious check for None in storage.py (_wordinfoset()). Tim Peters 2003-06-27 Fix a bug in storage.py (_wordinfoget()) which could cause incorrect token counts. Tony Meyer 2003-06-23 In imapfilter, try to append without flags if appending fails. Tony Meyer 2003-06-23 Implement SF#755098 - "Progress Indicator in imapfilter" Tony Meyer 2003-06-23 Fix the -i switch in imapfilter. Barry Warsaw 2003-06-22 Don't try and get password from options if -p is specified in imapfilter. Barry Warsaw 2003-06-22 Fix an import error in imapfilter. Release 1.0 Alpha 3 =================== Tony Meyer 2003-06-22 Fix RFC822.PEEK error in imapfilter. Neale Pickett 2003-06-21 "In hammiefilter, make untrain mode work." Mark Hammond 2003-06-20 "In Outlook plugin, add a 'verbose' option to the options." Mark Hammond 2003-06-19 "In Outlook plugin, change ""Anti-Spam Manager"" -> ""SpamBayes Manager""" Mark Hammond 2003-06-18 "In Outlook plugin, allow either spam or unsure messages to be marked as read as they are filtered." Mark Hammond 2003-06-18 "In Outlook plugin, more work on the toolbar." Mark Hammond 2003-06-17 2 new [General] options for Outlook plugin - delete_as_spam_message_state and recover_from_spam_message_state. Mark Hammond 2003-06-17 "In Outlook plugin, prevent ""Deleted items"", Outbox and Sent Items from appearing in the folder list. Fixes: [ 749277 ] Should prevent ""Deleted Items"" being target folder." Mark Hammond 2003-06-17 "In Outlook plugin, catch the assertion error in the classifier, and warn the user their database is corrupt." Mark Hammond 2003-06-17 "In Outlook plugin, introduce ReportErrorOnce, useful for errors on event handlers that you don't want to bombard the user with." Mark Hammond 2003-06-17 "In Outlook plugin, an error on a single sub-folder need not be fatal. Hopefully fixes [ 743515 ] Unable to expand folders in folder selection dialog." Mark Hammond 2003-06-17 "In Outlook plugin, DeleteAsSpam didn't detect ""no folder"" correctly." Anthony Baxter 2003-06-17 "In IMAPFilter interface, fix parm_map code." Mark Hammond 2003-06-16 Split OptionsClass into a separate file. Mark Hammond 2003-06-16 Allow lists to be used for multi-valued options. Mark Hammond 2003-06-16 "Allow the first entry in the ""defaults"" table to be a sub-class of Option." Mark Hammond 2003-06-16 "In Outlook plugin, when training, instead of suggesting the inbox, suggest folders we watch." Mark Hammond 2003-06-16 "In Outlook plugin, change ""Anti-Spam"" on the dropdown to ""SpamBayes""" Mark Hammond 2003-06-16 "In Outlook plugin, huge changes to configuration. No longer use a pickle, but instead a series of .INI files." Tony Meyer 2003-06-12 Update pop3proxy and imapfilter to print out version information. Skip Montanaro 2003-06-10 Correction to VM mailer addition to spambayes.el Tony Meyer 2003-06-08 "Add pop3proxy, hammie, smtpproxy & imapfilter version information." Neale Pickett 2003-06-06 Integrated code for the VM mailer into spambayes.el Mark Hammond 2003-06-05 Add a version information repository. Mark Hammond 2003-06-05 Change Outlook plugin to use new version information repository. Mark Hammond 2003-06-04 "In Outlook plugin, no longer default to the ""Inbox"" (see also SF#741797 ('Does not filter incoming mail'))." Mark Hammond 2003-06-04 "In Outlook plugin, when filtering, saving the spam score is no longer fatal." Tony Meyer 2003-06-04 "In muttrc, fix incorrect Spambayes header name." Mark Hammond 2003-06-03 Handle malformed messages better in the Outlook plugin. Mark Hammond 2003-06-03 "In Outlook plugin, create our own toolbar, rather than using the standard one." Mark Hammond 2003-06-03 Fix an Outlook plugin error that would try to save the database when classifying. Mark Hammond 2003-06-03 "Make Outlook plugin log refer to ""Spambayes"" rather than ""SpamAddin""" Mark Hammond 2003-06-03 Use 'wait' cursor when incremental training in Outlook plugin. Mark Hammond 2003-06-03 "In Outlook plugin, save config when dialog closes and not at shutdown." Mark Hammond 2003-06-03 Clean up toolbar images. Tony Meyer 2003-05-30 Added URL slurper. Mark Hammond 2003-05-29 "DB classifier keeps a list of ""changed words"" to prevent saves from updating *all* words." Mark Hammond 2003-05-29 DB classifier doesn't cache hapaxes. Tony Meyer 2003-05-26 Fix SF#737986 ('Message.as_string() fails.'). Tony Meyer 2003-05-26 Restore notate_to and notate_subject functionality to pop3proxy. Tony Meyer 2003-05-25 Expose experimental ham/spam imbalance option to pop3proxy/imapfilter users Tim Peters 2003-05-25 Restore __slots__ declaration to WordInfo object in classifier.py Tim Peters 2003-05-25 PickledClassifier.load(): use getstate/setstate to copy the state. Tony Meyer 2003-05-21 Cosmetic changes to pop3proxy and imapfilter interface. Tony Meyer 2003-05-21 "Remove the ""account for string nham/nspam"" code in classifier." Tim Peters 2003-05-19 Catch BoundaryError when parsing messages. Tim Peters 2003-05-19 Improve tokenisation for messages that have text that looks like Wrinkle Reduction Tim Peters 2003-05-19 Decode numeric character entities in html. Tim Peters 2003-05-19 Replace

and
tags with single blanks. Mark Hammond 2003-05-15 The training dialog now shows a correct progress bar for the *complete* operation - training *and* scoring Mark Hammond 2003-05-15 Fix SF#706170 ('Execute test suite fails in Outlook'). Mark Hammond 2003-05-15 Save bsddb databases after a training operation (should prevent Outlook ever saving at shutdown). Mark Hammond 2003-05-15 Print how long each save takes (so people can complain). Mark Hammond 2003-05-15 Keep and print some stats about how many items SpamBayes saw. Mark Hammond 2003-05-14 Various changes to urlslurper.py Mark Hammond 2003-05-14 Fix SF#737956 ('No hourglass when building folder lists'). Mark Hammond 2003-05-14 Fix SF#737955 ('Transient connection error disables plugin'). Tony Meyer 2003-05-14 Added missing import to UserInterface.py Tony Meyer 2003-05-14 Increased efforts to stop browsers caching the interface pages. Tony Meyer 2003-05-13 Added refresh button to review messages page in pop3proxy ui. Tony Meyer 2003-05-13 "Added ""check again"" link on 'no more untrained messages page' in pop3proxy ui." Tony Meyer 2003-05-13 "Added ""html_ui"":""display_to"" option." Tony Meyer 2003-05-11 Fixed KeyError bug in message.py. Tony Meyer 2003-05-11 Added SSL support to imapfilter (untested). Tony Meyer 2003-05-06 Added getattr backwards compatibility for renamed options. Tony Meyer 2003-05-06 IMAP Filter now deletes existing spambayes headers before training. Tony Meyer 2003-05-06 "Added ""Storage"":""messageinfo_storage_file"" option." Tony Meyer 2003-05-06 Fix for SF#733247 ('crash when using merged-in options'). Tim Stone 2003-05-04 Catch exception when a file in the pop3proxy cache mysteriously disappears. Tony Meyer 2003-05-04 Fix bug stopping some messages loading into pop3proxy cache. Tony Meyer 2003-05-03 Removed OptionConfig.py Mark Hammond 2003-05-03 Correct usage doc with respect to default directory. Mark Hammond 2003-05-03 Fix SF#715248 ('Pickle classifier should save to a temp file first'). Mark Hammond 2003-05-03 Formalized error reporting in Outlook plugin. Mark Hammond 2003-05-03 "Created special handling for ""startup errors"" in Outlook plugin" Mark Hammond 2003-05-03 Allow test suite to work with bsddb3 or bsddb. Tony Meyer 2003-05-03 Fix invalid options names in IMAP and POP3 user interface. Tony Meyer 2003-05-03 Fix incorrect state information displaying after configuration. Tony Meyer 2003-05-02 Fix temp file problem with web interface on linux. Tony Meyer 2003-05-02 Update configuration file reading so that writing uses same delimiters. Tony Meyer 2003-05-02 Fix temp file problem with web interface on linux. Mark Hammond 2003-05-01 Ignore the pywin.dialogs package in the Outlook plugin install. Mark Hammond 2003-05-01 Save the database after an explicit train. Mark Hammond 2003-05-01 Fix globals statements in URL slurper. Mark Hammond 2003-05-01 Fix testtools import in URL slurper. Mark Hammond 2003-05-01 Print URL slurper status to stderr. Mark Hammond 2003-05-01 Added socket.error catching in URL slurper. Mark Hammond 2003-05-01 Avoid slurping non html content. Tony Meyer 2003-05-01 Fix bug where URL cache would not be in the current working directory by default. Tony Meyer 2003-04-28 Stop using IMAP uids as our ids. Tony Meyer 2003-04-28 Fix SelectFolder bug in IMAP Filter. Tim Stone 2003-04-28 Create IMAP session object for each login. Tony Meyer 2003-04-28 Fix for SF#728886 ('In the pop3 UI not able to pass more than 1 server'). Tony Meyer 2003-04-28 Fix for incorrect is_boolean code in Options.py Tony Meyer 2003-04-28 "IMAP Filter now only retrieves RFC822 headers when iterating, not whole message." Tim Stone 2003-04-28 Moved CRLF replacement from IMAP to generic message class. Tony Meyer 2003-04-27 Minor bug fixes to Options.py Tony Meyer 2003-04-27 Fixed configuration file reading bug forcing single space separators instead of any whitespace. Tony Meyer 2003-04-27 Fixed Options.py bug reported by Tim Stone. Tony Meyer 2003-04-27 Fix hammiefilter calling mergefiles instead of merge_files. Tony Meyer 2003-04-25 Added message_from_string functions to message.py and imapfilter.py Tony Meyer 2003-04-25 Extra error checking in IMAP Filter. Tony Meyer 2003-04-24 "Fix SF#725307 (""Outlook plugin won't load (anymore)"")." Tony Meyer 2003-04-24 Fix SF#725466 ('Include a proper locale fix in Options.py'). Tony Meyer 2003-04-24 Fix SF#726255 ('Problem if bayescustomize.ini not there'). Tony Meyer 2003-04-24 Moved the CRLF fixing from generic message class to IMAP filter. Tony Meyer 2003-04-24 Major rewrite of Options.py Tony Meyer 2003-04-24 Changed options with multiple values to tuples. Tony Meyer 2003-04-24 Fixed bug where the IMAP user interface would try to display a folder before logging in. Tony Meyer 2003-04-24 Added convert_config_file script. Tim Stone 2003-04-24 Fix message.py incorrectly persisting/restoring state. Tony Meyer 2003-04-24 Fix Options.py convert and unconvert functions. Tony Meyer 2003-04-24 Fix valid characters for IMAP username and password. Tony Meyer 2003-04-24 Improve behaviour of convert_config_file script. Tony Meyer 2003-04-24 Check that spam/unsure/filter folders exist in IMAP before filtering/training. Tony Meyer 2003-04-24 Removed CompatConfigParser.py Tony Meyer 2003-04-24 Removed UpdatableConfigParser.py Tony Meyer 2003-04-22 Fixed is_valid method for sets. Tim Stone 2003-04-22 Added extra verbose output in IMAP Filter. Tim Stone 2003-04-22 Corrected counting error in IMAP Filter. Tony Meyer 2003-04-22 Fix SF#725616 ('Options.py mergefiles crashes (+ fix)'). Tony Meyer 2003-04-22 Improved processing of fetch response in IMAP filter. Tim Stone 2003-04-21 Cosmetic changes to the web configuration page. Skip Montanaro 2003-04-21 Fix CRLF regex in message.py Tony Meyer 2003-04-21 Fix SF#725307 ('Outlook plugin won't load (anymore)'). Tim Stone 2003-04-21 Rewrote is_valid method in Options.py Tony Meyer 2003-04-20 Moved pop3proxy test code into separate module. Tony Meyer 2003-04-20 "Allow """" as a valid option if the valid values are defined by a regex and multiple values are allowed." Tony Meyer 2003-04-20 Change imapfilter server option to pop3proxy style (i.e. server[:port]) Tony Meyer 2003-04-20 Set web interface pages to not cache. Tony Meyer 2003-04-20 Fixed imapfilter and pop3proxy web configuration. Tony Meyer 2003-04-20 Change the select folder config for imapfilter to two pages. Tony Meyer 2003-04-20 Added check for parsedate failing in imapfilter. Tony Meyer 2003-04-20 Fix the regex for the date in imapfilter. Tim Stone 2003-04-20 Removed unnecessary imports/comments in pop3proxy. Tim Stone 2003-04-20 Changed pop3proxy's STAT handling to return a guess at new message size. Tony Meyer 2003-04-19 Fixed updating of configuration files. Tim Stone 2003-04-19 Correction to regex for smtpproxy servers validation. Tony Meyer 2003-04-19 Stop IMAP filter filtering messages marked as deleted. Tony Meyer 2003-04-19 Copy IMAP flags along with message. Tony Meyer 2003-04-19 Use IMAP date instead of the local one. Tony Meyer 2003-04-19 Added utility functions to Options.py to assist people to figure out what is available. Tony Meyer 2003-04-18 "Remove support for pop3proxy_port, pop3proxy_server_name and pop3proxy_server_port options (long deprecated)." Tony Meyer 2003-04-18 Remove option to launch smtpproxy and always do this from pop3proxy (if servers are specified). Tony Meyer 2003-04-18 Fix smtpproxy bug that would prevent mail sent in the same session as training mail being delivered. Tony Meyer 2003-04-18 Major rework of configuration file reading/options. Section names no longer ignored. Tony Meyer 2003-04-18 "Moved interface code out of pop3proxy into ImapUI, ProxyUI and UserInterface.py" Tim Stone 2003-04-18 Added web interface support to imapfilter. Tim Stone 2003-04-18 Only start browser if required in imapfilter. Tony Meyer 2003-04-18 Fix for time retrieval in imapfilter when the date header is missing. Tony Meyer 2003-04-18 Change the line endings fix (pop3proxy/imapfilter) to a more robust one (from smtplib). Tim Stone 2003-04-18 Converted pop3proxy to use message.py. (notate-body is no longer working) Mark Hammond 2003-04-18 Fixed Outlook plugin to work in non-English locales. Tim Stone 2003-04-17 Corrected newline mangling in imapfilter. Tim Stone 2003-04-17 Corrected folder comparison operator in imapfilter. Tim Stone 2003-04-17 Refactored functionality into an IMAPSession class. Tim Stone 2003-04-17 Added -p option for password prompt in imapfilter. Tim Stone 2003-04-17 Corrected imapfilter training error which would result in no training being done. Tim Stone 2003-04-17 Corrected an error in the timed loop that unnecessarily kept IMAP sessions open. Tim Stone 2003-04-17 Moved the header repaid regex into the message class. Tony Meyer 2003-04-17 Added ConfigParser from Python 2.3 which solves many problems in the 2.2 version. Tony Meyer 2003-04-17 Updated UpdatableConfigParser to use new 2.3 ConfigParser. Tony Meyer 2003-04-16 Fix an imapfilter invalid date problem. Tim Stone 2003-04-14 Fixed the imapfilter docstring not printing. Tim Stone 2003-04-14 Changed imapfilter -e argument to y/n. Tim Stone 2003-04-14 Added -l argument to imapfilter allowing looping. Tim Stone 2003-04-14 Correctly extract timestamp for new messages in imapfilter. Tim Stone 2003-04-13 Raise an error if message.py is asked to remember an unknown classification. Tim Stone 2003-04-13 General cleanup of imapfilter. Tim Stone 2003-04-13 Remove old message ids from the message info db when using imapfilter. Skip Montanaro 2003-04-13 Fix train() in mboxtrain. Tony Meyer 2003-04-13 Fix for user interface showing incorrect server strings when using pop3proxy_service. Tony Meyer 2003-04-13 Various speed improvements to imapfilter. Tim Stone 2003-04-12 Functional version of the imapfilter. Tim Stone 2003-04-10 Made base message class more abstract and added a SBHeaderMessage class. Tim Stone 2003-04-10 Made imapfilter use the new message class. Tony Meyer 2003-04-09 Update imapfilter to reflect message class changes. Tony Meyer 2003-04-08 Introduce a IMAPMessage class based on the spambayes Message class. Tony Meyer 2003-04-08 Introduce an iterable IMAPFolder class. Tony Meyer 2003-04-08 Allow filtering of multiple folders in IMAP filter. Tim Stone 2003-04-08 Added methods to message.py to support copying one message to another. Tim Stone 2003-04-08 Added logic to imapfilter to ensure that classification and training memory is preserved. Tony Meyer 2003-04-07 First check-in of IMAP filter. Tim Stone 2003-04-07 First check-in of message.py Tim Stone 2003-04-07 Changed imapfilter to use the message class. Tony Meyer 2003-04-03 Expire messages from the unknown pop3proxy cache as well as ham/spam caches. Tony Meyer 2003-04-03 "Kick off expiry check each time a client connects to the proxy rather than on startup, for those who have the proxy stable!" Tony Meyer 2003-04-03 "Add ""show clues"" button to the review message page in pop3proxy ui." Tim Stone 2003-03-28 Unicode print error fix in notesfilter. Mark Hammond 2003-03-23 Fix SF#707491 ('Pop3 proxy service code for Windows doesn't work...'). Mark Hammond 2003-03-20 Ensure database is saved before testing outlook plugin. Mark Hammond 2003-03-20 Fix error in testing outlook plugin (getting wrong end of sorted list). Tim Stone 2003-03-20 Added SF#703283 ('mboxtrain only trains on cur in maildir'). Mark Hammond 2003-03-18 Ensure all buttons are greyed during filter process in Outlook plugin. Mark Hammond 2003-03-18 When running Outlook plugin from a binary redirect output to a log. Mark Hammond 2003-03-18 Version 002 of the binary. Mark Hammond 2003-03-17 "Warn, but ignore errors walking the folder tree in Outlook plugin." Mark Hammond 2003-03-17 "Fix SF#704921 ('""Train now"" (outlook) fails ')." Mark Hammond 2003-03-17 Prevent a single error filtering a message from stopping the whole filter process in Outlook plugin. Mark Hammond 2003-03-16 Allow messages to be scored after training in Outlook plugin. Mark Hammond 2003-03-16 Fix bug in Outlook plugin where filter operation report was reporting incorrect total. Tony Meyer 2003-03-13 Revert Options.py to ConfigParser from UpdatableConfigParser. Mark Hammond 2003-03-12 Added pop3proxy_service.py Tony Meyer 2003-03-12 "Update web ui so that boolean options are radio buttons rather than text boxes, and multiple choice options are checkboxes." Tony Meyer 2003-03-12 Separated out pop3proxy options and header options in pop3proxy web ui. Tony Meyer 2003-03-12 Adds UpdatableConfigParser. Tony Meyer 2003-03-12 Changes Options.py to use UpdatableConfigParser rather than ConfigParser. Tony Meyer 2003-03-11 Fixes a bug in pop3proxy/smtpproxy where headers will appear in the message body. Tony Meyer 2003-03-10 Added level & evidence headers in pop3proxy. Mark Hammond 2003-03-09 "Correct ""data"" directory location in export.py." Tony Meyer 2003-03-09 Added pop3proxy include_prob option to (optionally) note the score/prob in the classification header. Tim Stone 2003-03-09 "Added configuration for pop3proxy include_prob option, add_mailid_to and strip_incoming mailIds options." Tim Stone 2003-03-08 Fix SF#700165 ('MoveFileEx doesn't exist on Win98'). Tim Peters 2003-03-08 List unique tokens one per line in Outlook plugin's ShowClues. Mark Hammond 2003-03-07 Handle MAPI exceptions better in Outlook plugin. Mark Hammond 2003-03-07 "Centralise detection of ""not found"" exceptions in Outlook plugin." Mark Hammond 2003-03-07 Suppress errors in outlook plugin when hotmail connector can't save the message. T. Alexander Popiel 2003-03-07 Added another regime to regimes.py. Tim Stone 2003-03-06 Added SF#690928 ('turn off saving messages in popproxy'). Skip Montanaro 2003-03-06 Catch extra exception in header parse errors. Tim Stone 2003-03-06 "Fix SF#698852 (""can't classify messages"")." Mark Hammond 2003-03-04 Fix a Outlook plugin bug that could cause incorrect word scores to be used/saved when a bsddb database is used. Mark Hammond 2003-03-04 Remove unused 'wordcache' instance variable in storage.py. Mark Hammond 2003-03-04 Show all tokens in message when showing clues in Outlook plugin. Mark Hammond 2003-03-04 "Fix a bug in the Outlook plugin test code that used *all* ham/spam words, not just top 50." Mark Hammond 2003-03-04 "Only remember source folder when filtering, not just scoring in Outlook plugin." Mark Hammond 2003-03-04 "Only attempt to create ""Spam"" field on a mail item in Outlook plugin." Mark Hammond 2003-03-04 If no items are found in Outlook plugin don't attempt to recurse folders. Mark Hammond 2003-03-04 Fix SF#696476 ('Manual filtering in outlook fails'). Mark Hammond 2003-03-04 Fix SF#697120 ('Manual filtering in Outlook (still) fails'). Jeremy Hylton 2003-03-04 Band-aid decode_header() in pop3proxy.py Mark Hammond 2003-03-03 Fix manager.py calling shutil.move (does not exist pre Python 2.3). Mark Hammond 2003-03-03 Fix SF#696995 ('Invalid HTML comments are not ignored'). Skip Montanaro 2003-03-03 Note when subject charset is invalid (rather than raising an exception). Tony Meyer 2003-03-02 Added support to smtpproxy for extracting an id in a Mozilla style forwarded message (HTML table). T. Alexander Popiel 2003-03-01 "Add option to mkgraph to spit out counts *or* error rates, and n-day averages *or* cumulative." Tony Meyer 2003-03-01 Small modifications to smtpproxy related pop3proxy options. Tony Meyer 2003-03-01 Expose options for adding an id to incoming mail in pop3proxy. Tony Meyer 2003-03-01 Expose options for using smtpproxy. T. Alexander Popiel 2003-02-28 Put all regimes into regimes.py. Define fpfnunsure and fnunsure regimes. T. Alexander Popiel 2003-02-28 Fix name conflict between regimes list and regimes source file in incremental.py T. Alexander Popiel 2003-02-28 Reduce the amount of progress output in incremental.py Tony Meyer 2003-02-27 "Add options to add an id (as a header or in the body), strip such ids from incoming mail, find a message via the id, train messages by forwarding/bouncing to smtpproxy." Tony Meyer 2003-02-27 First check in of smtpproxy. T. Alexander Popiel 2003-02-27 "Added various files relating to incremental training (or ""self training"") regimes." Tim Stone 2003-02-27 Fix SF#693423 ('email message generates error in pop3proxy.py'). Mark Hammond 2003-02-26 "Fix bug in dbmstorage.py for Python 2.2, which tried bsddb3 twice if it failed." Tim Stone 2003-02-25 First check in of Lotus Notes filter. Tim Stone 2003-02-25 Add option for pop3proxy to notate Subject: header. Tony Meyer 2003-02-25 Fix bug in Corpus.get() which would never return the default value. Mark Hammond 2003-02-18 "Store Outlook plugin files in the ""correct"" Windows directory." Neil Schemenauer 2003-02-16 Add -c and -d options to mailsort.py. Neil Schemenauer 2003-02-16 First check-in of dump_cdb.py Mark Hammond 2003-02-13 Add SF#685746 ('Outlook plugin folder list sorted alphabetically'). Mark Hammond 2003-02-13 Handle exceptions when opening folders in Outlook plugin better. Skip Montanaro 2003-02-13 Split BAYESCUSTOMIZE environment variable using os.pathsep. Mark Hammond 2003-02-12 Check for correct exception when removing file in Outlook addin. Mark Hammond 2003-02-12 Check for bsddb3 before bsddb (previously bsddb3 would never be found). Tim Stone 2003-02-10 Changed BAYESCUSTOMIZE environment variable parsing from a split to a regex to fix filenames with embedded spaces. Tim Stone 2003-02-08 Ensure that nham and nspam are instances of integer in dbExpImp.py Tim Stone 2003-02-08 Ensure that nham and nspam becoming strings doesn't break classification. Tim Stone 2003-02-08 Added ability to put classification in subject or to headers (for OE). Mark Hammond 2003-02-07 Fix some errors using bsddb3 in Outlook plugin. Mark Hammond 2003-02-04 "Fix SF#642740 ('""Recover from Spam"" wrong folder')." Mark Hammond 2003-02-03 Change train.py to be able to work with a bsddb database. Mark Hammond 2003-02-03 If a new bsddb or bsddb3 module is available use this instead of a pickle in the Outlook plugin. Mark Hammond 2003-02-03 Add tick-marks to the filter dialog. Mark Hammond 2003-02-03 Fix SF#677804 ('Untouched filter command error'). spambayes-1.1a6/contrib/0000775000076500000240000000000011355064626015331 5ustar skipstaff00000000000000spambayes-1.1a6/contrib/BULK.txt0000664000076500000240000001106510646440136016626 0ustar skipstaff00000000000000Alex's spambayes filter scripts ------------------------------- I've finally started using spambayes for my incoming mail filtering. I've got a slightly unusual setup, so I had to write a couple scripts to deal with the nightly retraining... First off, let me describe how I've got things set up. I am an avid (and rather religious) MH user, so my mail folders are of course stored in the MH format (directories full of single-message files, where the filenames are numbers indicating ordering in the folder). I've got four mail folders of interest for this discussion: everything, spam, newspam, and inbox. When mail arrives, it is classified, then immediately copied in the everything folder. If it was classified as spam or ham, it is trained as such, reinforcing the classification. Then, if it was labeled as spam, it goes into the newspam folder; otherwise it goes into my inbox. When I read my mail (from inbox or newspam), I move any confirmed spam into my spam folder; ham may be deleted. (Of course, I still have a copy of my ham in the everything folder.) Every night, I run a complete retraining (from cron at 2:10am); it trains on all mail in the everything folder that is less than 4 months old. If a given message has an identical copy in the spam or newspam folder, then it is trained as spam; otherwise it is trained as ham. This does mean that unread unsures will be treated as ham for up to a day; there's few enough of them that I don't care. The four-month age limit will have the effect of expiring old mail out of the training set, which will keep the database size fairly manageable (it's currently just under 10 meg, with 6 days to go until I have 4 months of data). The retraining generates a little report for me each night, showing a graph of my ham and spam levels over time. Here's a sample: | Scanning spamdir (/home/cashew/popiel/Mail/spam): | Scanning spamdir (/home/cashew/popiel/Mail/newspam): | Scanning everything | sshsshsshsshsshsshsshshsshshshshsshshshshshshsshsshshsshssshsshshsshshsshshs | sshshshshsshshsshshshshshssshshshsshsshsshshshshshshsshshhshshsshshshshssshs | sshshsssshs | 154 | 152| | 144| | 136| | 128| h | 120| h s | 112| s ss ss s h s ss | 104| ss ss ss sHs h s ss | 96| s ss s sH s ss sHs h Sss ss | 88| h ss s sss ss sH sss ssssHHhS sSsssss | 80| s sSH ss ssssss sssssH HssssHsHHHSS sSsssss | 72| ssHSH ssssssssssssHHsHSHssHsHsHHHSSssSsssss | 64| s s s s sHsHSHsssssssHsHsssHHsHSHssHsHsHHHSSssSsssss | 56| s sss ss sssssHHHSHsHsssHsHHHHssHHsHSHHsHHHsHHHSSsHSsssss | 48| ssssssssssssssHHHSHHHHssHsHHHHHsHHsHSHHsHHHsHHHSSsHSssHsss | 40| ssssssssssHsHHHHHSHHHHHsHsHHHHHHHHHHSHHsHHHHHHHSSsHSHsHHss | 32| ssHHssHsssHHHHHHHSHHHHHHHsHHHHHHHHHHSHHsHHHHHHHSSHHSHHHHHs | 24| ssHHHHHHHsHHHHHHHSHHHHHHHsHHHHHHHHHHSHHHHHHHHHHSSHHSHHHHHs | 16| HsHHHHHHHHHHHHHHHSHHHHHHHHHHHHHHHHHHSHHHHHHHHHHSSHHSHHHHHs | 8| HHHHHHHHHHHHHHHHHSHHHHHHHHHHHHHHHHHHSHHHHHHHHHHSSHHSHHHHHH | 0|SSSUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU | +------------------------------------------------------------ | | Total: 6441 ham, 9987 spam (60.79% spam) | | real 7m45.049s | user 5m38.980s | sys 0m39.170s At the top of the output it mentions what it's scanning, and has a long line of s and h indicating progress (so it doesn't look hung if you run it by hand). Below is a set of overlaid bar graphs; s is for spam, h is for ham, u is unsure. The shorter bars are in front and capitalized. In the example, I have very few days where I have more ham than spam. Finally, there's the amount of time it took to run the retraining. My scripts are: bulkgraph.py read and train on messages, and generate the graph bulktrain.sh wrapper for bulkgraph.py, times the process and moves databases around procmailrc a slightly edited version of my .procmailrc file When I actually use this, I put bulkgraph.py and bulktrain.py in the root of my spambayes tree. Minor tweaks would probably make this unnecessary, but as a python newbie I don't know what they are off the top of my head, and I can't be bothered to find out. ;-) spambayes-1.1a6/contrib/bulkgraph.py0000664000076500000240000002022011112670703017644 0ustar skipstaff00000000000000#! /usr/bin/env python ### Train spambayes on messages in an MH mailbox, with spam identified ### by identical copies in other designated MH mailboxes. ### ### Run this from a cron job on your server. """Usage: %(program)s [OPTIONS] ... Where OPTIONS is one or more of: -h show usage and exit -d DBNAME use the DBM store. A DBM file is larger than the pickle and creating it is slower, but loading it is much faster, especially for large word databases. Recommended for use with hammiefilter or any procmail-based filter. -D DBNAME use the pickle store. A pickle is smaller and faster to create, but much slower to load. Recommended for use with pop3proxy and hammiesrv. -e PATH directory of all messages (both ham and spam). -s PATH directory of known spam messages to train on. These should be duplicates of messages in the everything folder. Can be specified more than once. -f force training, ignoring the trained header. Use this if you need to rebuild your database from scratch. -q quiet mode; no output """ import getopt import sys import os import re import time import filecmp from spambayes import mboxutils, hammie program = sys.argv[0] loud = True day = 24 * 60 * 60 # The following are in days expire = 4 * 30 grouping = 2 def usage(code, msg=''): """Print usage message and sys.exit(code).""" if msg: print >> sys.stderr, msg print >> sys.stderr print >> sys.stderr, __doc__ % globals() sys.exit(code) def row(value, spamday, hamday, unsureday): line = "%5d|" % value for j in range(((expire) // grouping) - 1, -1, -1): spamv = 0 hamv = 0 unsurev = 0 for k in range(j * grouping, (j + 1) * grouping): try: spamv += spamday[k] hamv += hamday[k] unsurev += unsureday[k] except: pass spamv = spamv // grouping hamv = hamv // grouping unsurev = unsurev // grouping # print "%d: %ds %dh %du" % (j, spamv, hamv, unsurev) count = 0 char = ' ' if spamv >= value: count += 1 char = 's' if hamv >= value: count += 1 if (char == ' ' or hamv < spamv): char = 'h' if unsurev >= value: count += 1 if (char == ' ' or (char == 's' and unsurev < spamv) or (char == 'h' and unsurev < hamv)): char = 'u' if count > 1: char = char.upper() line += char return line def legend(): line = " " * 60 now = time.mktime(time.strptime(time.strftime("%d %b %Y"), "%d %b %Y")) date = time.mktime(time.strptime(time.strftime("1 %b %Y"), "%d %b %Y")) age = int(59 - ((now - date) // day // grouping)) if age >= 55: line = line[:age] + time.strftime("| %b") else: line = line[:(age)] + "|" + line[(age+1):] center = int((age + 59) // 2) line = line[:center] + time.strftime("%b") + line[center+3:] date = time.mktime(time.strptime(time.strftime("1 %b %Y", time.localtime(date - day * 2)), "%d %b %Y")) newage = int(59 - ((now - date) // day // grouping)) while newage >= 0: line = line[:newage] + "|" + line[newage+1:] center = int((age + newage) // 2) line = line[:center] + time.strftime("%b", time.localtime(date)) + line[center+3:] age = newage date = time.mktime(time.strptime(time.strftime("1 %b %Y", time.localtime(date - day * 2)), "%d %b %Y")) newage = int(59 - ((now - date) // day // grouping)) if age >= 4: center = int((age) // 2) line = line[:center-2] + time.strftime("%b", time.localtime(date)) + line[center+1:] return line def main(): """Main program; parse options and go.""" global loud try: opts, args = getopt.getopt(sys.argv[1:], 'hfqd:D:s:e:') except getopt.error, msg: usage(2, msg) if not opts: usage(2, "No options given") pck = None usedb = None force = False everything = None spam = [] for opt, arg in opts: if opt == '-h': usage(0) elif opt == "-f": force = True elif opt == "-q": loud = False elif opt == '-e': everything = arg elif opt == '-s': spam.append(arg) elif opt == "-d": usedb = True pck = arg elif opt == "-D": usedb = False pck = arg if args: usage(2, "Positional arguments not allowed") if usedb == None: usage(2, "Must specify one of -d or -D") h = hammie.open(pck, usedb, "c") spamsizes = {} for s in spam: if loud: print "Scanning spamdir (%s):" % s files = os.listdir(s) for f in files: if f[0] in ('1', '2', '3', '4', '5', '6', '7', '8', '9'): name = os.path.join(s, f) size = os.stat(name).st_size try: spamsizes[size].append(name) except KeyError: spamsizes[size] = [name] skipcount = 0 spamcount = 0 hamcount = 0 spamday = [0] * expire hamday = [0] * expire unsureday = [0] * expire date_re = re.compile( r";.* (\d{1,2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{2,4})") now = time.mktime(time.strptime(time.strftime("%d %b %Y"), "%d %b %Y")) if loud: print "Scanning everything" for f in os.listdir(everything): if f[0] in ('1', '2', '3', '4', '5', '6', '7', '8', '9'): name = os.path.join(everything, f) fh = file(name, "rb") msg = mboxutils.get_message(fh) fh.close() # Figure out how old the message is age = 2 * expire try: received = (msg.get_all("Received"))[0] received = date_re.search(received).group(1) # if loud: print " %s" % received date = time.mktime(time.strptime(received, "%d %b %Y")) # if loud: print " %d" % date age = (now - date) // day # Can't just continue here... we're in a try if age < 0: age = 2 * expire except: pass # Skip anything that has no date or is too old or from the future # if loud: print "%s: %d" % (name, age) if age >= expire: skipcount += 1 if loud and not (skipcount % 100): sys.stdout.write("-") sys.stdout.flush() continue age = int(age) try: if msg.get("X-Spambayes-Classification").find("unsure") >= 0: unsureday[age] += 1 except: pass size = os.stat(name).st_size isspam = False try: for s in spamsizes[size]: if filecmp.cmp(name, s): isspam = True except KeyError: pass if isspam: spamcount += 1 spamday[age] += 1 if loud and not (spamcount % 100): sys.stdout.write("s") sys.stdout.flush() else: hamcount += 1 hamday[age] += 1 if loud and not (hamcount % 100): sys.stdout.write("h") sys.stdout.flush() h.train(msg, isspam) if loud: print mval = max(max(spamday), max(hamday), max(unsureday)) scale = (mval + 19) // 20 print "%5d" % mval for j in range(19, -1, -1): print row(scale * j, spamday, hamday, unsureday) print " +" + ('-' * 60) print " " + legend() print print "Total: %d ham, %d spam (%.2f%% spam)" % ( hamcount, spamcount, spamcount * 100.0 / (hamcount + spamcount)) h.store() if __name__ == "__main__": main() spambayes-1.1a6/contrib/bulktrain.sh0000775000076500000240000000035610646440136017663 0ustar skipstaff00000000000000#!/bin/bash cd $HOME/spambayes/active/spambayes rm -f tmpdb 2>/dev/null time /usr/bin/python2.2 bulkgraph.py \ -d tmpdb \ -e $HOME/Mail/everything/ \ -s $HOME/Mail/spam \ -s $HOME/Mail/newspam \ && mv -f tmpdb hammiedb ls -l hammiedb spambayes-1.1a6/contrib/findbest.py0000664000076500000240000001672611112670675017513 0ustar skipstaff00000000000000#!/usr/bin/env python ''' Find the next "best" unsure message to train on. %(prog)s [ -h ] [ -s ] [ -b N ] ham spam unsure Given a number of unsure messages and a desire to keep your training database small, the question naturally arises, "Which message should I add to my database next?". A common approach is to sort the unsures by their SpamBayes scores and train on the one which scores lowest. This is a reasonable approach, but there is no guarantee the lowest scoring unsure is in any way related to the other unsure messages. This script offers a different approach. Given an existing pile of ham and spam, it trains on them to establish a baseline, then for each message in the unsure pile, it trains on that message, scores the entire unsure pile against the resulting training database, then untrains on that message. For each such message the following output is generated: * spamprob of the candidate message * number of other unsure messages which would score as spam if it was added to the training database * overall mean of all scored messages after training * standard deviation of all scored messages after training * message-id of the candidate message With no options, all candidate unsure messages are trained and scored against. At the end of the run, a file, "best.pck" is written out which is a dictionary keyed by the overall mean rounded to three decimal places. The values are lists of message-ids which generate that mean. Three options affect the behavior of the program. If the -h flag is given, this help message is displayed and the program exits. If the -s flag is given, no messages which score as spam are tested as candidates. If the -b N flag is given, only the messages which generated the N highest means in the last run without the -b flag are tested as candidates. Because the program runtime can be very slow (O(n^2) in the number of unsure messages), if you have a fairly large pile of unsure messages, these options can speed things up dramatically. If the -b flag is used, a new "best.pck" file is not written. Typically you would run once without the -b flag, then several times with the -b flag, adding one message to the spam pile after each run. After adding several messages to your spam file, you might then redistribute the unsure pile to move spams and hams to their respective folders, then start again with a smaller unsure pile. The ham, spam and unsure command line arguments can be anything suitable for feeding to spambayes.mboxutils.getmbox(). The "best.pck" file is searched for and written to these files in this order: * best.pck in the current directory * $HOME/tmp/best.pck * $HOME/best.pck [To do? Someone might consider the reverse operation. Given a pile of ham and spam, which message can be removed with the least impact? What pile of mail should that removal be tested against?] ''' import sys import os import getopt import math from spambayes.mboxutils import getmbox from spambayes.classifier import Classifier from spambayes.hammie import Hammie from spambayes.tokenizer import tokenize from spambayes.Options import options from spambayes import storage from spambayes.safepickle import pickle_read, pickle_write cls = Classifier() h = Hammie(cls) def counter(tag, i): if tag: sys.stdout.write("\r%s: %4d" % (tag, i)) else: sys.stdout.write("\r%4d" % i) sys.stdout.flush() def learn(mbox, h, is_spam): i = 0 tag = is_spam and "Spam" or "Ham" for msg in getmbox(mbox): counter(tag, i) i += 1 h.train(msg, is_spam) print def score(unsure, h, cls, scores, msgids=None, skipspam=False): """See what effect on others each msg in unsure has""" spam_cutoff = options["Categorization", "spam_cutoff"] # compute a base - number of messages in unsure already in the # region of interest n = 0 total = 0.0 okalready = set() add = okalready.add for msg in getmbox(unsure): prob = cls.spamprob(tokenize(msg)) n += 1 if prob >= spam_cutoff: add(msg['message-id']) else: total += prob first_mean = total/n print len(okalready), "out of", n, "messages already score as spam" print "initial mean spam prob: %.3f" % first_mean print "%5s %3s %5s %5s %s" % ("prob", "new", "mean", "sdev", "msgid") # one by one, train on each message and see what effect it has on # the other messages in the mailbox for msg in getmbox(unsure): msgid = msg['message-id'] if msgids is not None and msgid not in msgids: continue msgprob = cls.spamprob(tokenize(msg)) if skipspam and msgprob >= spam_cutoff: continue n = j = 0 h.train(msg, True) # see how many other messages in unsure now score as spam total = 0.0 probs = [] for trial in getmbox(unsure): # don't score messages which previously scored as spam if trial['message-id'] in okalready: continue n += 1 if n % 10 == 0: counter("", n) prob = cls.spamprob(tokenize(trial)) probs.append(prob) total += prob if prob >= spam_cutoff: j += 1 counter("", n) h.untrain(msg, True) mean = total/n meankey = round(mean, 3) scores.setdefault(meankey, []).append(msgid) sdev = math.sqrt(sum([(mean-prob)**2 for prob in probs])/n) print "\r%.3f %3d %.3f %.3f %s" % (msgprob, j, mean, sdev, msgid) prog = os.path.basename(sys.argv[0]) def usage(msg=None): if msg is not None: print >> sys.stderr, msg print >> sys.stderr, __doc__.strip() % globals() def main(args): try: opts, args = getopt.getopt(args, "b:sh") except getopt.error, msg: usage(msg) return 1 best = 0 skipspam = False for opt, arg in opts: if opt == "-h": usage() return 0 if opt == "-b": best = int(arg) elif opt == "-s": skipspam = True if len(args) != 3: usage("require ham, spam and unsure message piles") return 1 ham, spam, unsure = args choices = ["best.pck"] if "HOME" in os.environ: home = os.environ["HOME"] choices.append(os.path.join(home, "tmp", "best.pck")) choices.append(os.path.join(home, "best.pck")) choices.append(None) for bestfile in choices: if bestfile is None: break if os.path.exists(bestfile): break try: file(bestfile, "w") except IOError: pass else: os.unlink(bestfile) if bestfile is None: usage("can't find a place to write best.pck file") return 1 print "establish base training" learn(ham, h, False) learn(spam, h, True) print "scoring" if best: last_scores = pickle_read(bestfile) last_scores = last_scores.items() last_scores.sort() msgids = set() for (k, v) in last_scores[-best:]: msgids.update(set(v)) else: msgids = None scores = {} try: score(unsure, h, cls, scores, msgids, skipspam) except KeyboardInterrupt: # allow early termination without loss of computed scores pass if not best: pickle_write(bestfile, scores) return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:])) spambayes-1.1a6/contrib/mod_spambayes.py0000664000076500000240000000161611112670704020521 0ustar skipstaff00000000000000## ## This is a simple Spambayes plugin for Amit Patel's proxy3 web proxy: ## http://theory.stanford.edu/~amitp/proxy.html ## ## Author: Skip Montanaro ## from proxy3_filter import * import proxy3_options from spambayes import hammie, Options dbf = Options.get_pathname_option("Storage", "persistent_storage_file") class SpambayesFilter(BufferAllFilter): checker = hammie.open(dbf, 1, 'r') def filter(self, s): if self.reply.split()[1] == '200': prob = self.checker.score("%s\r\n%s" % (self.serverheaders, s)) print "| prob: %.5f" % prob if prob >= Options.options["Categorization", "spam_cutoff"]: print self.serverheaders print "text:", s[0:40], "...", s[-40:] return "not authorized" return s from proxy3_util import * register_filter('*/*', 'text/html', SpambayesFilter) spambayes-1.1a6/contrib/muttrc0000664000076500000240000000277010646440136016574 0ustar skipstaff00000000000000## ## Mutt keybindings for spambayes ## Author: Neale Pickett ## ## This muttrc assumes you are already filtering with a procmail recipie ## similar to: ## ## :0fw ## | sb_filter.py -t ## ## ## This binds 'S' to train on the current message as spam, and 'H' to ## train on the current message as ham. Both of these commands ## re-classify the message and send it through procmail, so you'll have ## two copies after running them. ## ## As a special bonus, all tagged spam will be colored red on black. ## ## If you have any problems with this, and especially if you have any ## improvements, please mail them to me! Thanks to Adam Hupp for ## helping out with the muttisms. ## macro index S "|sb_filter.py -s -f | procmail\n" macro pager S "|sb_filter.py -s -f | procmail\n" macro index H "|sb_filter.py -g -f | procmail\n" macro pager H "|sb_filter.py -g -f | procmail\n" color index red black "~h 'X-Spambayes-Disposition: spam' ~F" ## If you're feeling bold and don't mind the possibility of losing mail, ## you can uncomment these lines. These bindings automatically delete ## the message in addition to retraining and sending through procmail. ## If there's a problem with sb_filter, though, the message will be lost ## forever. ## #macro index S "|sb_filter.py -s -f | procmail\n" #macro pager S "|sb_filter.py -s -f | procmail\n" #macro index H "|sb_filter.py -g -f | procmail\n" #macro pager H "|sb_filter.py -g -f | procmail\n" spambayes-1.1a6/contrib/nway.py0000664000076500000240000000627511112670702016660 0ustar skipstaff00000000000000#!/usr/bin/env python """ Demonstration of n-way classification possibilities. Usage: %(prog)s [ -h ] tag=db ... -h - print this message and exit. All args are of the form 'tag=db' where 'tag' is the tag to be given in the X-Spambayes-Classification: header. A single message is read from stdin and a modified message sent to stdout. The message is compared against each database in turn. If its score exceeds the spam threshold when scored against a particular database, an X-Spambayes-Classification header is added and the modified message is written to stdout. If none of the comparisons yields a definite classification, the message is written with an 'X-Spambayes-Classification: unsure' header. Training is left up to the user. In general, you want to train so that a message in a particular category will score above your spam threshold when checked against that category's training database. For example, suppose you have the following mbox formatted files: python, music, family, cars. If you wanted to create a training database for each of them you could execute this series of mboxtrain.py commands: sb_mboxtrain.py -f -d python.db -s python -g music -g family -g cars sb_mboxtrain.py -f -d music.db -g python -s music -g family -g cars sb_mboxtrain.py -f -d family.db -g python -g music -s family -g cars sb_mboxtrain.py -f -d cars.db -g python -g music -g family -s cars You'd then compare messages using a %(prog)s command like this: %(prog)s python=python.db music=music.db family=family.db cars=cars.db Normal usage (at least as I envisioned it) would be to run the program via procmail or something similar. You'd then have a .procmailrc file which looked something like this: :0 fw:sb.lock | $(prog)s spam=spam.db python=python.db music=music.db ... :0 * ^X-Spambayes-Classification: spam spam :0 * ^X-Spambayes-Classification: python python :0 * ^X-Spambayes-Classification: music music ... :0 * ^X-Spambayes-Classification: unsure unsure Note that I've not tried this (yet). It should simplify the logic in a .procmailrc file and probably classify messages better than writing more convoluted procmail rules. """ import getopt import sys import os from spambayes import hammie, mboxutils, Options prog = os.path.basename(sys.argv[0]) def usage(): print >> sys.stderr, __doc__ % globals() def main(args): opts, args = getopt.getopt(args, "h") for opt, arg in opts: if opt == '-h': usage() return 0 msg = mboxutils.get_message(sys.stdin) try: del msg["X-Spambayes-Classification"] except KeyError: pass for pair in args: tag, db = pair.split('=', 1) h = hammie.open(db, True, 'r') score = h.score(msg) if score >= Options.options["Categorization", "spam_cutoff"]: msg["X-Spambayes-Classification"] = "%s; %.2f" % (tag, score) break else: msg["X-Spambayes-Classification"] = "unsure" sys.stdout.write(msg.as_string(unixfrom=(msg.get_unixfrom() is not None))) return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:])) spambayes-1.1a6/contrib/procmailrc0000664000076500000240000000135210646440136017404 0ustar skipstaff00000000000000MAILDIR=/home/cashew/popiel/Mail HOME=/home/cashew/popiel # Classify message (up here so all copies have the classification) :0fw: | /usr/bin/python2.2 $HOME/spambayes/active/spambayes/hammiefilter.py # And trust the classification :0Hc: * ^X-Spambayes-Classification: ham | /usr/bin/python2.2 $HOME/spambayes/active/spambayes/hammiefilter.py -g :0Hc: * ^X-Spambayes-Classification: spam | /usr/bin/python2.2 $HOME/spambayes/active/spambayes/hammiefilter.py -s # Save all mail for analysis :0c: everything/. # Block spam :0H: * ^Content-Type:.*text/html newspam/. :0H: * ^X-Spambayes-Classification: spam newspam/. # Put mail from myself in outbox :0H: * ^From:.*popiel\@wolfskeep outbox/. # Everything else is presumably good :0: inbox/. spambayes-1.1a6/contrib/pycksum.py0000664000076500000240000001272211116631531017370 0ustar skipstaff00000000000000#!/usr/bin/env python """ A fuzzy checksum program based on a message posted to the spambayes list a long time ago from Justin Mason of the SpamAssassin gang. The basic idea is that you dump stuff that can be obviously variable (email addresses and such), compute several partial checksums over what remains, then compare pieces against previous partial checksums to make a decision about a match. Note that this concept can break down for small messages. I only use it downstream from Spambayes - after it's scored the message as spam: :0 * ^X-Spambayes-Classification: (.*-)?spam { ### this recipe gobbles items with matching body checksums (taken ### loosely to try and avoid obvious tricks) :0 W: cksum.lock | pycksum.py -v $HOME/tmp/cksum.cache 2>> $HOME/tmp/cksum.log ... further spam processing here } That reduces the risk of tossing out mail I'm actually interested in. ;-) I run it in verbose mode and save the log message. It catches a fair fraction of duplicate spams, probably 3 out of every 4. (Mail for several email addresses funnels into skip@mojam.com.) """ # message on stdin # cmdline arg is db file to store checksums # exit status is designed to fit into procmail's idea of delivery - exiting # with a 0 implies the message is a duplicate and the message is deemed # delivered - exiting with a 1 implies the message hasn't been seen before import getopt import sys import email.Parser import email.generator import anydbm import re import time try: import cStringIO as StringIO except ImportError: import StringIO from spambayes.port import md5 def clean(data): """Clean the obviously variable stuff from a chunk of data. The first (and perhaps only) use of this is to try and eliminate bits of data that keep multiple spam email messages from looking the same. """ # Get rid of anything which looks like an HTML tag and downcase it all data = re.sub(r"<[^>]*>", "", data).lower() # Map all digits to '#' data = re.sub(r"[0-9]+", "#", data) # Map a few common html entities data = re.sub(r"( )+", " ", data) data = re.sub(r"<", "<", data) data = re.sub(r">", ">", data) data = re.sub(r"&", "&", data) # Elide blank lines and multiple horizontal whitespace data = re.sub(r"\n+", "\n", data) data = re.sub(r"[ \t]+", " ", data) # delete anything which looks like a url or email address # not sure what a pmguid: url is but it seems to occur frequently in spam # also convert all runs of whitespace into a single space return " ".join([w for w in data.split(" ") if ('@' not in w and (':' not in w or w[:4] != "ftp:" and w[:7] != "mailto:" and w[:5] != "http:" and w[:7] != "gopher:" and w[:8] != "pmguid:"))]) def generate_checksum(msg): # modelled after Justin Mason's fuzzy checksummer for SpamAssassin. # Message body is cleaned, then broken into lines. The list of lines is # then broken into four parts and separate checksums are generated for # each part. They are then joined together with '.'. Downstream # processes can split those chunks into pieces and consider them # separately or in various combinations if desired. fp = StringIO.StringIO() g = email.generator.Generator(fp, mangle_from_=False, maxheaderlen=60) g.flatten(msg) text = fp.getvalue() body = text.split("\n\n", 1)[1] lines = clean(body).split("\n") chunksize = len(lines)//4+1 digest = [] for i in range(4): chunk = "\n".join(lines[i*chunksize:(i+1)*chunksize]) digest.append(md5(chunk).hexdigest()) return ".".join(digest) def save_checksum(cksum, f): pieces = cksum.split('.') result = 1 db = anydbm.open(f, "c") maxdblen = 2**14 # consider the first two pieces, the middle two pieces and the last two # pieces - one or more will likely eliminate attempts at disrupting the # checksum - if any are found in the db file, call it a match for subsum in (".".join(pieces[:-2]), ".".join(pieces[1:-1]), ".".join(pieces[2:])): if not db.has_key(subsum): db[subsum] = str(time.time()) if len(db) > maxdblen: items = [(float(db[k]), k) for k in db.keys()] items.sort() # the -20 brings us down a bit below the max so we aren't # constantly running this chunk of code items = items[:-(maxdblen-20)] for v, k in items: del db[k] else: result = 0 break db.close() return result def main(args): opts, args = getopt.getopt(args, "v") verbose = 0 for opt, arg in opts: if opt == "-v": verbose = 1 if not args: dbf = None else: dbf = args[0] msg = email.Parser.Parser().parse(sys.stdin) cksum = generate_checksum(msg) if dbf is None: print cksum result = 1 disp = 'nodb' else: result = save_checksum(cksum, dbf) disp = result and 'old' or 'new' if verbose: t = time.strftime("%Y-%m-%d:%H:%M:%S", time.localtime(time.time())) logmsg = "%s/%s/%s/%s\n" % (t, cksum, disp, msg['message-id']) sys.stderr.write(logmsg) return result if __name__ == "__main__": sys.exit(main(sys.argv[1:])) spambayes-1.1a6/contrib/sb_culler.py0000664000076500000240000003726011112670700017650 0ustar skipstaff00000000000000#!/usr/bin/env python """sb_culler.py -- remove spam from POP3 servers, leave ham. I get about 150 spams a day and 12 viruses as background noise. I use Apple's Mail.app on my laptop, which filters out most of them. But when I travel my mailbox starts to accumulate crap, which is annoying over dial-up. Even at home, during peak periods of a recent virus shedding I got about 30 viruses an hour, and my 10MB mailbox filled up while I slept! I have a server machine at home, which can stay up full time. This program, sb_culler, uses SpamBayes to run a POP3 email culler. It connects to my email servers every few minutes, downloads the emails, classifies each one, and deletes the spam and viruses. (It makes a local copy of the spam, just in case.) This program is designed for me, a programmer. The structure should be helpful enough for other programmers, but even configuration must be done by editing the code. The virus identification and POP3 manipulation code is based on Kevin Altis' virus killer code, which I've been gratefully using for the last several months. Written by Andrew Dalke, November 2003. Released into the public domain on 2003/11/22. Updated 2004/10/26 == NO copyright protection asserted for this code. Share and enjoy! == This program requires Python 2.3 or newer. """ import socket socket.setdefaulttimeout(10) import traceback, md5, os import poplib import posixpath import sets from email import Header, Utils from spambayes import mboxutils, hammie from spambayes.Options import options DO_ACTIONS = 1 VERBOSE_LEVEL = 1 APPEND_TO_FILE = "append_to_file" DELETE_FROM_MAILBOX = "delete" KEEP_IN_MAILBOX = "keep in mailbox" SPAM = "spam" VIRUS = "virus" class Logger: def __init__(self): self.tests = {} self.actions = {} def __nonzero__(self): return bool(self.tests) and bool(self.actions) def pass_test(self, name): self.tests[name] = self.tests.get(name, 0) + 1 def do_action(self, name): self.actions[name] = self.actions.get(name, 0) + 1 def accept(self, text): print text def info(self, text): print text class MessageInfo: """reference to an email message in a mailbox""" def __init__(self, mailbox, i, msg, text): self.mailbox = mailbox self.i = i self.msg = msg self.text = text class Filter: """if message passes test then do the given action""" def __init__(self, test, action): self.test = test self.action = action def process(self, mi, log): result = self.test(mi, log) if result: self.action(mi, log) return self.action.descr + " because " + result return False class AppendFile: """Action: append message text to the given filename""" def __init__(self, filename): self.filename = filename self.descr = "save to %r then delete" % self.filename def __call__(self, mi, log): log.do_action(APPEND_TO_FILE) if not DO_ACTIONS: return f = open(self.filename, "a") try: f.write(mi.text) finally: f.close() mi.mailbox.dele(mi.i) def DELETE(mi, log): """Action: delete message from mailbox""" log.do_action(DELETE_FROM_MAILBOX) if not DO_ACTIONS: return mi.mailbox.dele(mi.i) DELETE.descr = "delete" def KEEP(mi, log): """Action: keep message in mailbox""" log.do_action(KEEP_IN_MAILBOX) KEEP.descr = "keep in mailbox" class Duplicate: def __init__(self): self.unique = {} def __call__(self, mi, log): digest = md5.md5(mi.text).digest() if digest in self.unique: log.pass_test(SPAM) return "duplicate" self.unique[digest] = 1 return False class IllegalDeliveredTo: def __init__(self, names): self.names = names def __call__(self, mi, log): fields = mi.msg.get_all("Delivered-To") if fields is None: return False for field in fields: field = field.lower() for name in self.names: if name in field: return False log.pass_test(SPAM) return "sent to random email" class SpamAssassin: def __init__(self, level = 8): self.level = level def __call__(self, mi, log): if ("*" * self.level) in mi.msg.get("X-Spam-Status", ""): log.pass_test(SPAM) return "assassinated!" return False class WhiteListFrom: """Test: Read a list of email addresses to use a 'from' whitelist""" def __init__(self, filename): self.filename = filename self._mtime = 0 self._load_if_needed() def _load(self): lines = [line.strip().lower() for line in open(self.filename).readlines()] self.addresses = sets.Set(lines) def _load_if_needed(self): mtime = os.path.getmtime(self.filename) if mtime != self._mtime: print "Reloading", self.filename self._mtime = mtime self._load() def __call__(self, mi, log): self._load_if_needed() frm = mi.msg["from"] realname, frm = Utils.parseaddr(frm) status = (frm is not None) and (frm.lower() in self.addresses) if status: log.pass_test(SPAM) return "it is in 'from' white list" return False class WhiteListSubstrings: """Test: Whitelist message if named field contains one of the substrings""" def __init__(self, field, substrings): self.field = field self.substrings = substrings def __call__(self, mi, log): data = mi.msg[self.field] if data is None: return False for s in self.substrings: if s in data: log.pass_test("'%s' white list" % (self.field,)) return "it matches '%s' white list" % (self.field,) return False class IsSpam: """Test: use SpamBayes to tell if something is spam""" def __init__(self, sb_hammie, spam_cutoff = None): self.sb_hammie = sb_hammie if spam_cutoff is None: spam_cutoff = options["Categorization", "spam_cutoff"] self.spam_cutoff = spam_cutoff def __call__(self, mi, log): prob = self.sb_hammie.score(mi.msg) if prob > self.spam_cutoff: log.pass_test(SPAM) return "it is spam (%4.3f)" % prob if VERBOSE_LEVEL > 1: print "not spam (%4.3f)" % prob return False # Simple check for executable attachments def IsVirus(mi, log): """Test: a virus is any message with an attached executable I've also noticed the viruses come in as wav and midi attachements so I trigger on those as well. This is a very paranoid detector, since someone might send me a binary for valid reasons. I white-list everyone who's sent me email before so it doesn't affect me. """ for part in mi.msg.walk(): if part.get_main_type() == 'multipart': continue filename = part.get_filename() if filename is None: if part.get_type() in ["application/x-msdownload", "audio/x-wav", "audio/x-midi"]: # Only viruses send messages to me with these types log.pass_test(VIRUS) return ("it has a virus-like content-type (%s)" % part.get_type()) else: extensions = "bat com exe pif ref scr vbs wsh".split() base, ext = posixpath.splitext(filename) if ext[1:].lower() in extensions: log.pass_test(VIRUS) return "it has a virus-like attachment (%s)" % ext[1:] return False def open_mailbox(server, username, password, debuglevel = 0): mailbox = poplib.POP3(server) try: mailbox.user(username) mailbox.pass_(password) mailbox.set_debuglevel(debuglevel) if VERBOSE_LEVEL > 1: count, size = mailbox.stat() print "Message count: ", count print "Total bytes : ", size except: mailbox.quit() raise return mailbox def _log_subject(mi, log): encoded_subject = mi.msg.get('subject') try: subject, encoding = Header.decode_header(encoded_subject)[0] except Header.HeaderParseError: log.info("%s Subject cannot be parsed" % (mi.i,)) return if encoding is None or encoding == 'iso-8859-1': s = subject else: s = encoded_subject log.info("%s Subject: %r" % (mi.i, s)) class Filters(list): def add(self, test, action): """short-cut to make a Filter given the test and action""" self.append(Filter(test, action)) def process_mailbox(self, mailbox): count, size = mailbox.stat() log = Logger() for i in range(1, count+1): if (i-1) % 10 == 0: print " == %d/%d ==" % (i, count) # Kevin's code used -1, but -1 doesn't work for one of # my POP accounts, while a million does. # Don't use retr because that may mark the message as # read (so says Kevin's code) message_tuple = mailbox.top(i, 1000000) text = "\n".join(message_tuple[1]) msg = mboxutils.get_message(text) mi = MessageInfo(mailbox, i, msg, text) _log_subject(mi, log) for filter in self: result = filter.process(mi, log) if result: log.accept(result) break else: # don't know what to do with this so just # keep it on the server log.pass_test("unknown") log.do_action(KEEP_IN_MAILBOX) log.accept("unknown") return log def filter_server( (server, user, pwd), filters): if VERBOSE_LEVEL: print "=" * 78 print "Processing %s on %s" % (user, server) mailbox = open_mailbox(server, user, pwd) try: log = filters.process_mailbox(mailbox) finally: mailbox.quit() return log ##### User-specific import time, sys, urllib # A simple text interface. def _unix_stop(): pass def _ms_stop(): # ^C doesn't seem to work correctly in the DOS box # so assume any keypress is a break if msvcrt.kbhit(): raise SystemExit() try: import msvcrt _check_for_stop = _ms_stop except ImportError: _check_for_stop = _unix_stop def restart_network(): # This is called after too many connection failures. # That usually means my ISP dropped my DHCP and I need to # bounce my Linksys firewall/DHCP/hub. print "Network appears to be down. Bringing Linksys down then up..." try: # Note this this example uses the default password. YMMV. urllib.urlopen("http://:admin@192.168.1.1/Gozila.cgi?pppoeAct=2").read() urllib.urlopen("http://:admin@192.168.1.1/Gozila.cgi?pppoeAct=1").read() except KeyboardInterrupt: raise except: traceback.print_exc() def wait(t, delta = 10): """Wait for 't' seconds""" assert delta > 0, delta assert t >= 1 first = True for i in range(t, -1, -delta): if VERBOSE_LEVEL: if not first: print "..", print i, sys.stdout.flush() time.sleep(min(i, delta)) _check_for_stop() first = False print def main(): filters = Filters() duplicate = Duplicate() filters.add(duplicate, AppendFile("spam2.mbox")) # A list of everyone who has emailed me this year. # Keep their messages on the server. filters.add(WhiteListFrom("good_emails.txt"), KEEP) # My mailing lists. filters.add(WhiteListSubstrings("subject", [ 'ABCD:', '[Python-announce]', '[Python]', '[Bioinfo]', '[EuroPython]', ]), KEEP) filters.add(WhiteListSubstrings("to", [ "president@whitehouse.gov", "ceo@big.com", ]), KEEP) names = ["john", "", "jon", "johnathan"] valid_emails = ([name + "@lectroid.com" for name in names] + [name + "@bigboote.org" for name in names] + ["buckeroo.bonzai@aol.earth"]) filters.add(IllegalDeliveredTo(valid_emails), DELETE) filters.add(SpamAssassin(), AppendFile("spam2.mbox")) # Get rid of anything which smells like an exectuable. filters.add(IsVirus, DELETE) # Use SpamBayes to identify spam. Make a local copy then # delete from the server. h = hammie.open("cull.spambayes", "dbm", "r") filters.add(IsSpam(h, 0.90), AppendFile("spam.mbox")) # These are my POP3 accounts. server_configs = [("mail.example.com", "user@example.com", "password"), ("popserver.big.com", "ceo", "12345"), ] # The main culling loop. error_count = 0 cumulative_log = {SPAM: 0, VIRUS: 0} initial_log = None start_time = None # init'ed only after initial_log is created while 1: error_flag = False duplicate.unique.clear() # Hack! for server, user, pwd in server_configs: try: log = filter_server( (server, user, pwd), filters) except KeyboardInterrupt: raw_input("Press enter to continue. ") except StandardError: raise except: error_flag = True traceback.print_exc() continue if VERBOSE_LEVEL > 1 and log: print " ** Summary **" for x in (log.tests, log.actions): items = x.items() if items: items.sort() for k, v in items: print " %s: %s" % (k, v) print cumulative_log[SPAM] += log.tests.get(SPAM, 0) cumulative_log[VIRUS] += log.tests.get(VIRUS, 0) if initial_log is None: initial_log = cumulative_log.copy() start_time = time.time() if VERBOSE_LEVEL: print "Stats: %d spams, %d virus" % ( initial_log[SPAM], initial_log[VIRUS]) else: if VERBOSE_LEVEL: delta_t = time.time() - start_time delta_t = max(delta_t, 1) # print "Stats: %d spams (%.2f/hr), %d virus (%.2f/hr)" % ( cumulative_log[SPAM], (cumulative_log[SPAM] - initial_log[SPAM]) / delta_t * 3600, cumulative_log[VIRUS], (cumulative_log[VIRUS] - initial_log[VIRUS]) / delta_t * 3600) if error_flag: error_count += 1 if error_count > 0: restart_network() error_count = 0 delay = 10 * 60 while delay: try: wait(delay) break except KeyboardInterrupt: print while 1: cmd = raw_input("enter, delay, or quit? ") if cmd in ("q", "quit"): raise SystemExit(0) elif cmd == "": delay = 0 break elif cmd.isdigit(): delay = int(cmd) break else: print "Unknown command." if __name__ == "__main__": main() spambayes-1.1a6/contrib/showclues.py0000775000076500000240000001234611116562760017725 0ustar skipstaff00000000000000#!/usr/bin/env python """Usage: showclues.py [options] [filenames] Options can one or more of: -h show usage and exit -d DBFILE use database in DBFILE -p PICKLEFILE use pickle (instead of database) in PICKLEFILE -m markup output with HTML -o section:option:value set [section, option] in the options database to value If no filenames are given on the command line, standard input will be processed as a single message. If one or more filenames are given on the command line, each will be processed according to the following rules: * If the filename is '-', standard input will be processed as a single message (may only be usefully given once). * If the filename starts with '+' it will be processed as an MH folder. * If the filename is a directory and it contains a subdirectory named 'cur', it will be processed as a Maildir. * If the filename is a directory and it contains a subdirectory named 'Mail', it will be processed as an MH Mailbox. * If the filename is a directory and not a Maildir nor an MH Mailbox, it will be processed as a Mailbox directory consisting of just .txt and .lorien files. * Otherwise, the filename is treated as a Unix-style mailbox (messages begin on a line starting with 'From '). """ # This module is part of the spambayes project, which is Copyright 2002-2007 # The Python Software Foundation and is covered by the Python Software # Foundation license. __author__ = "Tony Meyer " __credits__ = "All the Spambayes folk." import cgi import sys import getopt from spambayes import storage from spambayes import mboxutils from spambayes.classifier import Set from spambayes.Options import options from spambayes.tokenizer import tokenize def ShowClues(bayes, msg, as_html=False): if as_html: heading = "

", "

" tt = "", "" br = "
" pre = "
", "
" strong = "", "" escape = cgi.escape code = "", "" wrapper = "\n\n """ + body + "" # Attach the source message to it # Using the original message has the side-effect of marking the original # as unread. Tried to make a copy, but the copy then refused to delete # itself. # And the "UnRead" property of the message is not reflected in the object # model (we need to "refresh" the message). Oh well. new_msg.Attachments.Add(item, constants.olByValue, DisplayName="Original Message") new_msg.Display() # Event function fired from the "Empty Spam Folder" UI item. def EmptySpamFolder(mgr): config = mgr.config.filter ms = mgr.message_store spam_folder_id = getattr(config, "spam_folder_id") try: spam_folder = ms.GetFolder(spam_folder_id) except ms.MsgStoreException: mgr.LogDebug(0, "ERROR: Unable to open the spam folder for emptying - " \ "spam messages were not deleted") else: try: if spam_folder.GetItemCount() > 0: message = _("Are you sure you want to permanently delete " \ "all items in the \"%s\" folder?") \ % spam_folder.name if mgr.AskQuestion(message): mgr.LogDebug(2, "Emptying spam from folder '%s'" % \ spam_folder.GetFQName()) import manager spam_folder.EmptyFolder(manager._GetParent()) else: mgr.LogDebug(2, "Spam folder '%s' was already empty" % \ spam_folder.GetFQName()) message = _("The \"%s\" folder is already empty.") % \ spam_folder.name mgr.ReportInformation(message) except: mgr.LogDebug(0, "Error emptying spam folder '%s'!" % \ spam_folder.GetFQName()) traceback.print_exc() def CheckLatestVersion(manager): from spambayes.Version import get_current_version, get_version, \ get_download_page, fetch_latest_dict app_name = "Outlook" ver_current = get_current_version() cur_ver_string = ver_current.get_long_version(ADDIN_DISPLAY_NAME) try: SetWaitCursor(1) latest = fetch_latest_dict() SetWaitCursor(0) ver_latest = get_version(app_name, version_dict=latest) latest_ver_string = ver_latest.get_long_version(ADDIN_DISPLAY_NAME) except: print "Error checking the latest version" traceback.print_exc() manager.ReportError( _("There was an error checking for the latest version\r\n" "For specific details on the error, please see the SpamBayes log" "\r\n\r\nPlease check your internet connection, or try again later") ) return print "Current version is %s, latest is %s." % (str(ver_current), str(ver_latest)) if ver_latest > ver_current: url = get_download_page(app_name, version_dict=latest) msg = _("You are running %s\r\n\r\nThe latest available version is %s" \ "\r\n\r\nThe download page for the latest version is\r\n%s" \ "\r\n\r\nWould you like to visit this page now?") \ % (cur_ver_string, latest_ver_string, url) if manager.AskQuestion(msg): print "Opening browser page", url os.startfile(url) else: msg = _("The latest available version is %s\r\n\r\n" \ "No later version is available.") % latest_ver_string manager.ReportInformation(msg) # A hook for whatever tests we have setup def Tester(manager): import tester # This is only used in source-code versions - so we may as well reload # the test suite to save shutting down Outlook each time we tweak it. reload(tester) try: print "Executing automated tests..." tester.test(manager) except: traceback.print_exc() print "Tests FAILED. Sorry about that. If I were you, I would do a full re-train ASAP" print "Please delete any test messages from your Spam, Unsure or Inbox/Watch folders first." # The "Spam" and "Not Spam" buttons # The event from Outlook's explorer that our folder has changed. class ButtonDeleteAsEventBase: def Init(self, manager, explorer): self.manager = manager self.explorer = explorer def Close(self): self.manager = self.explorer = None class ButtonDeleteAsSpamEvent(ButtonDeleteAsEventBase): def OnClick(self, button, cancel): msgstore = self.manager.message_store msgstore_messages = self.explorer.GetSelectedMessages(True) if not msgstore_messages: return # If we are not yet enabled, tell the user. # (This is better than disabling the button as a) the user may not # understand why it is disabled, and b) as we would then need to check # the button state as the manager dialog closes. if not self.manager.config.filter.enabled: self.manager.ReportError( _("You must configure and enable SpamBayes before you " \ "can mark messages as spam")) return SetWaitCursor(1) # Delete this item as spam. spam_folder = None # It is unlikely that the spam folder is not specified, as the UI # will prevent enabling. But it could be invalid. spam_folder_id = self.manager.config.filter.spam_folder_id if spam_folder_id: try: spam_folder = msgstore.GetFolder(spam_folder_id) except msgstore.MsgStoreException: pass if spam_folder is None: self.manager.ReportError(_("You must configure the Spam folder"), _("Invalid Configuration")) return import train new_msg_state = self.manager.config.general.delete_as_spam_message_state for msgstore_message in msgstore_messages: # Record this recovery in our stats. self.manager.stats.RecordTraining(False, self.manager.score(msgstore_message)) # Record the original folder, in case this message is not where # it was after filtering, or has never been filtered. msgstore_message.RememberMessageCurrentFolder() msgstore_message.Save() # Must train before moving, else we lose the message! subject = msgstore_message.GetSubject() print "Moving and spam training message '%s' - " % (subject,), TrainAsSpam(msgstore_message, self.manager, save_db = False) # Do the new message state if necessary. try: if new_msg_state == "Read": msgstore_message.SetReadState(True) elif new_msg_state == "Unread": msgstore_message.SetReadState(False) else: if new_msg_state not in ["", "None", None]: print "*** Bad new_msg_state value: %r" % (new_msg_state,) except pythoncom.com_error: print "*** Failed to set the message state to '%s' for message '%s'" % (new_msg_state, subject) # Now move it. msgstore_message.MoveToReportingError(self.manager, spam_folder) # Note the move will possibly also trigger a re-train # but we are smart enough to know we have already done it. # And if the DB can save itself incrementally, do it now self.manager.classifier_data.SavePostIncrementalTrain() SetWaitCursor(0) class ButtonRecoverFromSpamEvent(ButtonDeleteAsEventBase): def OnClick(self, button, cancel): msgstore = self.manager.message_store msgstore_messages = self.explorer.GetSelectedMessages(True) if not msgstore_messages: return # If we are not yet enabled, tell the user. # (This is better than disabling the button as a) the user may not # understand why it is disabled, and b) as we would then need to check # the button state as the manager dialog closes. if not self.manager.config.filter.enabled: self.manager.ReportError( _("You must configure and enable SpamBayes before you " \ "can mark messages as not spam")) return SetWaitCursor(1) # Get the inbox as the default place to restore to # (incase we dont know (early code) or folder removed etc app = self.explorer.Application inbox_folder = msgstore.GetFolder( app.Session.GetDefaultFolder(constants.olFolderInbox)) new_msg_state = self.manager.config.general.recover_from_spam_message_state for msgstore_message in msgstore_messages: # Recover where they were moved from # During experimenting/playing/debugging, it is possible # that the source folder == dest folder - restore to # the inbox in this case. # (But more likely is that the original store may be read-only # so we were unable to record the initial folder, as we save it # *before* we do the move (and saving after is hard)) try: subject = msgstore_message.GetSubject() self.manager.classifier_data.message_db.load_msg(msgstore_message) restore_folder = msgstore_message.GetRememberedFolder() if restore_folder is None or \ msgstore_message.GetFolder() == restore_folder: print "Unable to determine source folder for message '%s' - restoring to Inbox" % (subject,) restore_folder = inbox_folder # Record this recovery in our stats. self.manager.stats.RecordTraining(True, self.manager.score(msgstore_message)) # Must train before moving, else we lose the message! print "Recovering to folder '%s' and ham training message '%s' - " % (restore_folder.name, subject), TrainAsHam(msgstore_message, self.manager, save_db = False) # Do the new message state if necessary. try: if new_msg_state == "Read": msgstore_message.SetReadState(True) elif new_msg_state == "Unread": msgstore_message.SetReadState(False) else: if new_msg_state not in ["", "None", None]: print "*** Bad new_msg_state value: %r" % (new_msg_state,) except msgstore.MsgStoreException, details: print "*** Failed to set the message state to '%s' for message '%s'" % (new_msg_state, subject) print details # Now move it. msgstore_message.MoveToReportingError(self.manager, restore_folder) except msgstore.NotFoundException: # Message moved under us - ignore. self.manager.LogDebug(1, "'Not Spam' had message moved from underneath us - ignored") # Note the move will possibly also trigger a re-train # but we are smart enough to know we have already done it. # And if the DB can save itself incrementally, do it now self.manager.classifier_data.SavePostIncrementalTrain() SetWaitCursor(0) # Helpers to work with images on buttons/toolbars. def SetButtonImage(button, fname, manager): # whew - http://support.microsoft.com/default.aspx?scid=KB;EN-US;q288771 # shows how to make a transparent bmp. # Also note that the clipboard takes ownership of the handle - # thus, we can not simply perform this load once and reuse the image. # Hacks for the binary - we can get the bitmaps from resources. if hasattr(sys, "frozen"): if fname=="recover_ham.bmp": bid = 6000 elif fname=="delete_as_spam.bmp": bid = 6001 else: raise RuntimeError, "What bitmap to use for '%s'?" % fname handle = win32gui.LoadImage(sys.frozendllhandle, bid, win32con.IMAGE_BITMAP, 0, 0, win32con.LR_DEFAULTSIZE) else: if not os.path.isabs(fname): # images relative to the application path fname = os.path.join(manager.application_directory, "images", fname) if not os.path.isfile(fname): print "WARNING - Trying to use image '%s', but it doesn't exist" % (fname,) return None handle = win32gui.LoadImage(0, fname, win32con.IMAGE_BITMAP, 0, 0, win32con.LR_DEFAULTSIZE | win32con.LR_LOADFROMFILE) win32clipboard.OpenClipboard() win32clipboard.SetClipboardData(win32con.CF_BITMAP, handle) win32clipboard.CloseClipboard() button.Style = constants.msoButtonIconAndCaption button.PasteFace() # A class that manages an "Outlook Explorer" - that is, a top-level window # All UI elements are managed here, and there is one instance per explorer. class ExplorerWithEvents: def Init(self, manager, explorers_collection): self.manager = manager self.have_setup_ui = False self.explorers_collection = explorers_collection self.toolbar = None def SetupUI(self): manager = self.manager assert self.toolbar is None, "Should not yet have a toolbar" # Add our "Spam" and "Not Spam" buttons tt_text = _("Move the selected message to the Spam folder,\n" \ "and train the system that this is Spam.") self.but_delete_as = self._AddControl( None, constants.msoControlButton, ButtonDeleteAsSpamEvent, (self.manager, self), Caption=_("Spam"), TooltipText = tt_text, BeginGroup = False, Tag = "SpamBayesCommand.DeleteAsSpam", image = "delete_as_spam.bmp") # And again for "Not Spam" tt_text = _(\ "Recovers the selected item back to the folder\n" \ "it was filtered from (or to the Inbox if this\n" \ "folder is not known), and trains the system that\n" \ "this is a good message\n") self.but_recover_as = self._AddControl( None, constants.msoControlButton, ButtonRecoverFromSpamEvent, (self.manager, self), Caption=_("Not Spam"), TooltipText = tt_text, Tag = "SpamBayesCommand.RecoverFromSpam", image = "recover_ham.bmp") # The main tool-bar dropdown with all our entries. # Add a pop-up menu to the toolbar # but loop around twice - first time we may find a non-functioning button popup = None for attempt in range(2): popup = self._AddControl( None, constants.msoControlPopup, None, None, Caption=_("SpamBayes"), TooltipText = _("SpamBayes anti-spam filters and functions"), Enabled = True, Tag = "SpamBayesCommand.Popup") if popup is None: # If the strategy below works for child buttons, we should # consider trying to re-create the top-level toolbar too. break # Convert from "CommandBarItem" to derived # "CommandBarPopup" Not sure if we should be able to work # this out ourselves, but no introspection I tried seemed # to indicate we can. VB does it via strongly-typed # declarations. popup = CastTo(popup, "CommandBarPopup") # And add our children. child = self._AddControl(popup, constants.msoControlButton, ButtonEvent, (manager.ShowManager,), Caption=_("SpamBayes Manager..."), TooltipText = _("Show the SpamBayes manager dialog."), Enabled = True, Visible=True, Tag = "SpamBayesCommand.Manager") # Only necessary to check the first child - if the first works, # the others will too if child is None: # Try and delete the popup, the bounce around the loop again, # which will re-create it. try: item = self.CommandBars.FindControl( Type = constants.msoControlPopup, Tag = "SpamBayesCommand.Popup") if item is None: print "ERROR: Could't re-find control to delete" break item.Delete(False) print "The above toolbar message is common - " \ "recreating the toolbar..." except pythoncom.com_error, e: print "ERROR: Failed to delete our dead toolbar control" break # ok - toolbar deleted - just run around the loop again continue self._AddControl(popup, constants.msoControlButton, ButtonEvent, (ShowClues, self.manager, self), Caption=_("Show spam clues for current message"), Enabled=True, Visible=True, Tag = "SpamBayesCommand.Clues") self._AddControl(popup, constants.msoControlButton, ButtonEvent, (manager.ShowFilterNow,), Caption=_("Filter messages..."), Enabled=True, Visible=True, Tag = "SpamBayesCommand.FilterNow") self._AddControl(popup, constants.msoControlButton, ButtonEvent, (EmptySpamFolder, self.manager), Caption=_("Empty Spam Folder"), Enabled=True, Visible=True, BeginGroup=True, Tag = "SpamBayesCommand.EmptySpam") self._AddControl(popup, constants.msoControlButton, ButtonEvent, (CheckLatestVersion, self.manager,), Caption=_("Check for new version"), Enabled=True, Visible=True, BeginGroup=True, Tag = "SpamBayesCommand.CheckVersion") helpPopup = self._AddControl( popup, constants.msoControlPopup, None, None, Caption=_("Help"), TooltipText = _("SpamBayes help documents"), Enabled = True, Tag = "SpamBayesCommand.HelpPopup") if helpPopup is not None: helpPopup = CastTo(helpPopup, "CommandBarPopup") self._AddHelpControl(helpPopup, _("About SpamBayes"), "about.html", "SpamBayesCommand.Help.ShowAbout") self._AddHelpControl(helpPopup, _("Troubleshooting Guide"), "docs/troubleshooting.html", "SpamBayesCommand.Help.ShowTroubleshooting") self._AddHelpControl(helpPopup, _("SpamBayes Website"), "http://spambayes.sourceforge.net/", "SpamBayesCommand.Help.ShowSpamBayes Website") self._AddHelpControl(helpPopup, _("Frequently Asked Questions"), "http://spambayes.sourceforge.net/faq.html", "SpamBayesCommand.Help.ShowFAQ") self._AddHelpControl(helpPopup, _("SpamBayes Bug Tracker"), "http://sourceforge.net/tracker/?group_id=61702&atid=498103", "SpamBayesCommand.Help.BugTacker") # If we are running from Python sources, enable a few extra items if not hasattr(sys, "frozen"): self._AddControl(popup, constants.msoControlButton, ButtonEvent, (Tester, self.manager), Caption=_("Execute test suite"), Enabled=True, Visible=True, BeginGroup=True, Tag = "SpamBayesCommand.TestSuite") self.have_setup_ui = True def _AddHelpControl(self, parent, caption, url, tag): self._AddControl(parent, constants.msoControlButton, ButtonEvent, (self.manager.ShowHtml, url), Caption=caption, Enabled=True, Visible=True, Tag=tag) def _AddControl(self, parent, # who the control is added to control_type, # type of control to add. events_class, events_init_args, # class/Init() args **item_attrs): # extra control attributes. # Outlook Toolbars suck :) # We have tried a number of options: temp/perm in the standard toolbar, # Always creating our own toolbar, etc. # This seems to be fairly common: # http://groups.google.com/groups?threadm=eKKmbvQvAHA.1808%40tkmsftngp02 # Now the strategy is just to use our own, permanent toolbar, with # permanent items, and ignore uninstall issues. # We search all commandbars for a control with our Tag. If found, we # use it (the user may have customized the bar and moved our buttons # elsewhere). If we can not find the child control, we then try and # locate our toolbar, creating if necessary. Our items get added to # that. assert item_attrs.has_key('Tag'), "Need a 'Tag' attribute!" image_fname = None if 'image' in item_attrs: image_fname = item_attrs['image'] del item_attrs['image'] tag = item_attrs["Tag"] item = self.CommandBars.FindControl( Type = control_type, Tag = tag) # we only create top-level items as permanent, so we keep a little control # over how they are ordered, especially between releases where the # subitems are subject to change. This will prevent the user # customising the dropdown items, but that is probably OK. # (we could stay permanent and use the 'before' arg, but this # is still pretty useless if the user has customized) temporary = parent is not None if item is not None and temporary: # oops - we used to create them perm, but item.Delete(False) item = None if item is None: if parent is None: # No parent specified - that means top-level - locate the # toolbar to use as the parent. if self.toolbar is None: # See if we can find our "SpamBayes" toolbar # Indexing via the name appears unreliable, so just loop # Pity we have no "Tag" on a toolbar - then we could even # handle being renamed by the user. bars = self.CommandBars for i in range(bars.Count): toolbar = bars.Item(i+1) if toolbar.Name == "SpamBayes": self.toolbar = toolbar break else: # for not broken - can't find toolbar. Create a new one. # Create it as a permanent one (which is default) print "Creating new SpamBayes toolbar to host our buttons" self.toolbar = bars.Add(toolbar_name, constants.msoBarTop, Temporary=False) self.toolbar.Visible = True parent = self.toolbar # Now add the item itself to the parent. try: item = parent.Controls.Add(Type=control_type, Temporary=temporary) except pythoncom.com_error, e: # Toolbars seem to still fail randomly for some users. # eg, bug [ 755738 ] Latest CVS outllok doesn't work print "FAILED to add the toolbar item '%s' - %s" % (tag,e) return if image_fname: # Eeek - only available in derived class. assert control_type == constants.msoControlButton but = CastTo(item, "_CommandBarButton") SetButtonImage(but, image_fname, self.manager) # Set the extra attributes passed in. for attr, val in item_attrs.items(): setattr(item, attr, val) # didn't previously set this, and it seems to fix alot of problem - so # we set it for every object, even existing ones. item.OnAction = "" # Hook events for the item, but only if we haven't already in some # other explorer instance. if events_class is not None and tag not in self.explorers_collection.button_event_map: item = DispatchWithEvents(item, events_class) item.Init(*events_init_args) # We must remember the item itself, else the events get disconnected # as the item destructs. self.explorers_collection.button_event_map[tag] = item return item def GetSelectedMessages(self, allow_multi = True, explorer = None): if explorer is None: explorer = self.Application.ActiveExplorer() sel = explorer.Selection if sel.Count > 1 and not allow_multi: self.manager.ReportError(_("Please select a single item"), _("Large selection")) return None ret = [] ms = self.manager.message_store for i in range(sel.Count): item = sel.Item(i+1) try: msgstore_message = ms.GetMessage(item) if msgstore_message.IsFilterCandidate(): ret.append(msgstore_message) except ms.NotFoundException: pass except ms.MsgStoreException, details: print "Unexpected error fetching message" traceback.print_exc() print details if len(ret) == 0: self.manager.ReportError(_("No filterable mail items are selected"), _("No selection")) return None if allow_multi: return ret return ret[0] # The Outlook event handlers def OnActivate(self): self.manager.LogDebug(3, "OnActivate", self) # See comments for OnNewExplorer below. # *sigh* - OnActivate seems too early too for Outlook 2000, # but Outlook 2003 seems to work here, and *not* the folder switch etc # Outlook 2000 crashes when a second window is created and we use this # event # OnViewSwitch however seems useful, so we ignore this. pass def OnSelectionChange(self): self.manager.LogDebug(3, "OnSelectionChange", self) # See comments for OnNewExplorer below. if not self.have_setup_ui: self.SetupUI() # Prime the button views. self.OnFolderSwitch() def OnClose(self): self.manager.LogDebug(3, "Explorer window closing", self) self.explorers_collection._DoDeadExplorer(self) self.explorers_collection = None self.toolbar = None self.close() # disconnect events. def OnBeforeFolderSwitch(self, new_folder, cancel): self.manager.LogDebug(3, "OnBeforeFolderSwitch", self) def OnFolderSwitch(self): self.manager.LogDebug(3, "OnFolderSwitch", self) # Yet another worm-around for our event timing woes. This may # be the first event ever seen for this explorer if, eg, # "Outlook Today" is the initial Outlook view. if not self.have_setup_ui: self.SetupUI() # Work out what folder we are in. outlook_folder = self.CurrentFolder if outlook_folder is None or \ outlook_folder.DefaultItemType != constants.olMailItem: show_delete_as = False show_recover_as = False else: show_delete_as = True show_recover_as = False try: mapi_folder = self.manager.message_store.GetFolder(outlook_folder) look_id = self.manager.config.filter.spam_folder_id if mapi_folder is not None and look_id: look_folder = self.manager.message_store.GetFolder(look_id) if mapi_folder == look_folder: # This is the Spam folder - only show "recover" show_recover_as = True show_delete_as = False # Check if uncertain look_id = self.manager.config.filter.unsure_folder_id if mapi_folder is not None and look_id: look_folder = self.manager.message_store.GetFolder(look_id) if mapi_folder == look_folder: show_recover_as = True show_delete_as = True except: print "Error finding the MAPI folders for a folder switch event" # As this happens once per move, we should only display it once. self.manager.ReportErrorOnce(_( "There appears to be a problem with the SpamBayes" " configuration\r\n\r\nPlease select the SpamBayes" " manager, and run the\r\nConfiguration Wizard to" " reconfigure the filter."), _("Invalid SpamBayes Configuration")) traceback.print_exc() if self.but_recover_as is not None: self.but_recover_as.Visible = show_recover_as if self.but_delete_as is not None: self.but_delete_as.Visible = show_delete_as def OnBeforeViewSwitch(self, new_view, cancel): self.manager.LogDebug(3, "OnBeforeViewSwitch", self) def OnViewSwitch(self): self.manager.LogDebug(3, "OnViewSwitch", self) if not self.have_setup_ui: self.SetupUI() # Events from our "Explorers" collection (not an Explorer instance) class ExplorersEvent: def Init(self, manager): assert manager self.manager = manager self.explorers = [] self.button_event_map = {} def Close(self): while self.explorers: self._DoDeadExplorer(self.explorers[0]) self.explorers = None def _DoNewExplorer(self, explorer): explorer = DispatchWithEvents(explorer, ExplorerWithEvents) explorer.Init(self.manager, self) self.explorers.append(explorer) return explorer def _DoDeadExplorer(self, explorer): self.explorers.remove(explorer) if len(self.explorers)==0: # No more explorers - disconnect all events. # (not doing this causes shutdown problems) for tag, button in self.button_event_map.items(): closer = getattr(button, "Close", None) if closer is not None: closer() self.button_event_map = {} def OnNewExplorer(self, explorer): # NOTE - Outlook has a bug, as confirmed by many on Usenet, in # that OnNewExplorer is too early to access the CommandBars # etc elements. We hack around this by putting the logic in # the first OnActivate call of the explorer itself. # Except that doesn't always work either - sometimes # OnActivate will cause a crash when selecting "Open in New Window", # so we tried OnSelectionChanges, which works OK until there is a # view with no items (eg, Outlook Today) - so at the end of the # day, we can never assume we have been initialized! self._DoNewExplorer(explorer) # The outlook Plugin COM object itself. class OutlookAddin: _com_interfaces_ = ['_IDTExtensibility2'] _public_methods_ = [] _reg_clsctx_ = pythoncom.CLSCTX_INPROC_SERVER _reg_clsid_ = "{3556EDEE-FC91-4cf2-A0E4-7489747BAB10}" _reg_progid_ = "SpamBayes.OutlookAddin" _reg_policy_spec_ = "win32com.server.policy.EventHandlerPolicy" def __init__(self): self.folder_hooks = {} self.application = None def OnConnection(self, application, connectMode, addin, custom): # Handle failures during initialization so that we are not # automatically disabled by Outlook. # Our error reporter is in the "manager" module, so we get that first locale.setlocale(locale.LC_NUMERIC, "C") # see locale comments above import manager try: self.application = application self.manager = None # if we die while creating it! # Create our bayes manager self.manager = manager.GetManager(application) assert self.manager.addin is None, "Should not already have an addin" self.manager.addin = self # Only now will the import of "spambayes.Version" work, as the # manager is what munges sys.path for us. from spambayes.Version import get_current_version v = get_current_version() vstring = v.get_long_version(ADDIN_DISPLAY_NAME) if not hasattr(sys, "frozen"): vstring += " from source" print vstring major, minor, spack, platform, ver_str = win32api.GetVersionEx() print "on Windows %d.%d.%d (%s)" % \ (major, minor, spack, ver_str) print "using Python", sys.version from time import asctime, localtime print "Log created", asctime(localtime()) self.explorers_events = None # create at OnStartupComplete if connectMode == constants.ext_cm_AfterStartup: # We are being enabled after startup, which means we don't get # the 'OnStartupComplete()' event - call it manually so we # bootstrap code that can't happen until startup is complete. self.OnStartupComplete(None) except: print "Error connecting to Outlook!" traceback.print_exc() # We can't translate this string, as we haven't managed to load # the translation tools. manager.ReportError( "There was an error initializing the SpamBayes addin\r\n\r\n" "Please re-start Outlook and try again.") def OnStartupComplete(self, custom): # Setup all our filters and hooks. We used to do this in OnConnection, # but a number of 'strange' bugs were reported which I suspect would # go away if done during this later event - and this later place # does seem more "correct" than the initial OnConnection event. if self.manager.never_configured: import dialogs dialogs.ShowWizard(0, self.manager) if self.manager.config.filter.enabled: # A little "sanity test" to help the user. If our status is # 'enabled', then it means we have previously managed to # convince the manager dialog to enable. If for some reason, # we no folder definitions but are 'enabled', then it is likely # something got hosed and the user doesn't know. # Note that we could display the config wizard here, but this # has rarely been reported in the wild since the very early # days, so could possibly die. if not self.manager.config.filter.spam_folder_id or \ not self.manager.config.filter.watch_folder_ids: msg = _("It appears there was an error loading your configuration\r\n\r\n" \ "Please re-configure SpamBayes via the SpamBayes dropdown") self.manager.ReportError(msg) # But continue on regardless. self.FiltersChanged() try: self.ProcessMissedMessages() except: print "Error processing missed messages!" traceback.print_exc() else: # We should include this fact in the log, as I suspect a # a number of "it doesn't work" bugs are simply related to not # being enabled. The new Wizard should help, but things can # still screw up. self.manager.LogDebug(0, _("*** SpamBayes is NOT enabled, so " \ "will not filter incoming mail. ***")) # Toolbar and other UI stuff must be setup once startup is complete. explorers = self.application.Explorers if self.manager is not None: # If we successfully started up. # and Explorers events so we know when new explorers spring into life. self.explorers_events = WithEvents(explorers, ExplorersEvent) self.explorers_events.Init(self.manager) # And hook our UI elements to all existing explorers for i in range(explorers.Count): explorer = explorers.Item(i+1) explorer = self.explorers_events._DoNewExplorer(explorer) explorer.OnFolderSwitch() def ProcessMissedMessages(self): from time import clock config = self.manager.config.filter manager = self.manager field_name = manager.config.general.field_score_name for folder in manager.message_store.GetFolderGenerator( config.watch_folder_ids, config.watch_include_sub): event_hook = self._GetHookForFolder(folder) # Note event_hook may be none in some strange cases where we # were unable to hook the events for the folder. This is # generally caused by a temporary Outlook issue rather than a # problem of ours we need to address. if event_hook is None: manager.LogDebug(0, "Skipping processing of missed messages in folder '%s', " "as it is not available" % folder.name) elif event_hook.use_timer: print "Processing missed spam in folder '%s' by starting a timer" \ % (folder.name,) event_hook._StartTimer() else: num = 0 start = clock() for message in folder.GetNewUnscoredMessageGenerator(field_name): ProcessMessage(message, manager) num += 1 # See if perf hurts anyone too much. print "Processing %d missed spam in folder '%s' took %gms" \ % (num, folder.name, (clock()-start)*1000) def FiltersChanged(self): try: # Create a notification hook for all folders we filter. self.UpdateFolderHooks() except: self.manager.ReportFatalStartupError( "Could not watch the specified folders") # UpdateFolderHooks takes care of ensuring the Outlook field exists # for all folders we watch - but we never watch the 'Unsure' # folder, and this one is arguably the most important to have it. unsure_id = self.manager.config.filter.unsure_folder_id if unsure_id: try: self.manager.EnsureOutlookFieldsForFolder(unsure_id) except: # If this fails, just log an error - don't bother with # the traceback print "Error adding field to 'Unsure' folder %r" % (unsure_id,) etype, value, tb = sys.exc_info() tb = None # dont want it, and nuke circular ref traceback.print_exception(etype, value, tb) def UpdateFolderHooks(self): config = self.manager.config.filter new_hooks = {} new_hooks.update( self._HookFolderEvents(config.watch_folder_ids, config.watch_include_sub, HamFolderItemsEvent, "filtering") ) # For spam manually moved if config.spam_folder_id and \ self.manager.config.training.train_manual_spam: new_hooks.update( self._HookFolderEvents([config.spam_folder_id], False, SpamFolderItemsEvent, "incremental training") ) for k in self.folder_hooks.keys(): if not new_hooks.has_key(k): self.folder_hooks[k].Close() self.folder_hooks = new_hooks def _GetHookForFolder(self, folder): ret = self.folder_hooks.get(folder.id) if ret is None: # we were unable to hook events for this folder. return None assert ret.target == folder return ret def _HookFolderEvents(self, folder_ids, include_sub, HandlerClass, what): new_hooks = {} for msgstore_folder in self.manager.message_store.GetFolderGenerator( folder_ids, include_sub): existing = self.folder_hooks.get(msgstore_folder.id) if existing is None or existing.__class__ != HandlerClass: name = msgstore_folder.GetFQName() try: folder = msgstore_folder.GetOutlookItem() except self.manager.message_store.MsgStoreException, details: # Exceptions here are most likely when the folder is valid # and available to MAPI, but not via the Outlook. # One good way to provoke this is to configure Outlook's # profile so default delivery is set to "None". Then, # when you start Outlook, it immediately displays an # error and terminates. During this process, the addin # is initialized, attempts to get the folders, and fails. print "FAILED to open the Outlook folder '%s' " \ "to hook events" % name print details continue # Ensure the field is created before we hook the folder # events, else there is a chance our event handler will # see the temporary message we create. try: self.manager.EnsureOutlookFieldsForFolder(msgstore_folder.GetID()) except: # An exception checking that Outlook's folder has a # 'spam' field is not fatal, nor really even worth # telling the user about, nor even worth a traceback # (as it is likely a COM error). print "ERROR: Failed to check folder '%s' for " \ "Spam field" % name etype, value, tb = sys.exc_info() tb = None # dont want it, and nuke circular ref traceback.print_exception(etype, value, tb) # now setup the hook. try: new_hook = DispatchWithEvents(folder.Items, HandlerClass) except ValueError: print "WARNING: Folder '%s' can not hook events" % (name,) new_hook = None if new_hook is not None: new_hook.Init(msgstore_folder, self.application, self.manager) new_hooks[msgstore_folder.id] = new_hook print "SpamBayes: Watching (for %s) in '%s'" % (what, name) else: new_hooks[msgstore_folder.id] = existing existing.ReInit() return new_hooks def OnDisconnection(self, mode, custom): print "SpamBayes - Disconnecting from Outlook" if self.folder_hooks: for hook in self.folder_hooks.values(): hook.Close() self.folder_hooks = None if self.explorers_events is not None: self.explorers_events.Close() self.explorers_events = None if self.manager is not None: # Save database - bsddb databases will generally do nothing here # as it will not be dirty, but pickles will. # config never needs saving as it is always done by whoever changes # it (ie, the dialog) self.manager.Save() # Report some simple stats, for session, and for total. print "Session:" print "\r\n".join(self.manager.stats.GetStats(session_only=True)) print "Total:" print "\r\n".join(self.manager.stats.GetStats()) self.manager.Close() self.manager = None if mode==constants.ext_dm_UserClosed: # The user has de-selected us. Remove the toolbars we created # (Maybe we can exploit this later to remove toolbars as part # of uninstall?) print "SpamBayes is being manually disabled - deleting toolbar" try: explorers = self.application.Explorers for i in range(explorers.Count): explorer = explorers.Item(i+1) try: toolbar = explorer.CommandBars.Item(toolbar_name) except pythoncom.com_error: print "Could not find our toolbar to delete!" else: toolbar.Delete() except: print "ERROR deleting toolbar" traceback.print_exc() self.application = None print "Addin terminating: %d COM client and %d COM servers exist." \ % (pythoncom._GetInterfaceCount(), pythoncom._GetGatewayCount()) try: # will be available if "python_d addin.py" is used to # register the addin. total_refs = sys.gettotalrefcount() # debug Python builds only print "%d Python references exist" % (total_refs,) except AttributeError: pass def OnAddInsUpdate(self, custom): pass def OnBeginShutdown(self, custom): pass def _DoRegister(klass, root): key = _winreg.CreateKey(root, "Software\\Microsoft\\Office\\Outlook\\Addins") subkey = _winreg.CreateKey(key, klass._reg_progid_) _winreg.SetValueEx(subkey, "CommandLineSafe", 0, _winreg.REG_DWORD, 0) _winreg.SetValueEx(subkey, "LoadBehavior", 0, _winreg.REG_DWORD, 3) _winreg.SetValueEx(subkey, "Description", 0, _winreg.REG_SZ, "SpamBayes anti-spam tool") _winreg.SetValueEx(subkey, "FriendlyName", 0, _winreg.REG_SZ, "SpamBayes") # Note that Addins can be registered either in HKEY_CURRENT_USER or # HKEY_LOCAL_MACHINE. If the former, then: # * Only available for the user that installed the addin. # * Appears in the 'COM Addins' list, and can be removed by the user. # If HKEY_LOCAL_MACHINE: # * Available for every user who uses the machine. This is useful for site # admins, so it works with "roaming profiles" as users move around. # * Does not appear in 'COM Addins', and thus can not be disabled by the user. # Note that if the addin is registered in both places, it acts as if it is # only installed in HKLM - ie, does not appear in the addins list. # For this reason, the addin can be registered in HKEY_LOCAL_MACHINE # by executing 'regsvr32 /i:hkey_local_machine outlook_addin.dll' # (or 'python addin.py hkey_local_machine' for source code users. # Note to Binary Builders: You need py2exe dated 8-Dec-03+ for this to work. # Called when "regsvr32 /i:whatever" is used. We support 'hkey_local_machine' def DllInstall(bInstall, cmdline): klass = OutlookAddin if bInstall and cmdline.lower().find('hkey_local_machine')>=0: # Unregister the old installation, if one exists. DllUnregisterServer() # Don't catch exceptions here - if it fails, the Dll registration # must fail. _DoRegister(klass, _winreg.HKEY_LOCAL_MACHINE) print "Registration (in HKEY_LOCAL_MACHINE) complete." def DllRegisterServer(): klass = OutlookAddin # *sigh* - we used to *also* register in HKLM, but as above, this makes # things work like we are *only* installed in HKLM. Thus, we explicitly # remove the HKLM registration here (but it can be re-added - see the # notes above.) try: _winreg.DeleteKey(_winreg.HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Office\\Outlook\\Addins\\" \ + klass._reg_progid_) except WindowsError: pass _DoRegister(klass, _winreg.HKEY_CURRENT_USER) print "Registration complete." def DllUnregisterServer(): klass = OutlookAddin # Try to remove the HKLM version. try: _winreg.DeleteKey(_winreg.HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Office\\Outlook\\Addins\\" \ + klass._reg_progid_) except WindowsError: pass # and again for current user. try: _winreg.DeleteKey(_winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Office\\Outlook\\Addins\\" \ + klass._reg_progid_) except WindowsError: pass if __name__ == '__main__': # woohoo - here is a wicked hack. If we are a frozen .EXE, then we are # a mini "registration" utility. However, we still want to register the # DLL, *not* us. Pretend we are frozen in that DLL. # NOTE: This is only needed due to problems with Inno Setup unregistering # our DLL the 'normal' way, but then being unable to remove the files as # they are in use (presumably by Inno doing the unregister!). If this # problem ever goes away, so will the need for this to be frozen as # an executable. In all cases other than as above, 'regsvr32 dll_name' # is still the preferred way of registering our binary. if hasattr(sys, "frozen"): sys.frozendllhandle = win32api.LoadLibrary("outlook_addin.dll") pythoncom.frozen = sys.frozen = "dll" # Without this, com registration will look at class.__module__, and # get all confused about the module name holding our class in the DLL OutlookAddin._reg_class_spec_ = "addin.OutlookAddin" # And continue doing the registration with our hacked environment. import win32com.server.register win32com.server.register.UseCommandLine(OutlookAddin) # todo - later win32all versions of UseCommandLine support # finalize_register and finalize_unregister keyword args, passing the # functions. # (But DllInstall may get support in UseCommandLine later, so let's # wait and see) if "--unregister" in sys.argv: DllUnregisterServer() else: DllRegisterServer() # Support 'hkey_local_machine' on the commandline, to work in # the same way as 'regsvr32 /i:hkey_local_machine' does. # regsvr32 calls it after DllRegisterServer, (and our registration # logic relies on that) so we will too. for a in sys.argv[1:]: if a.lower()=='hkey_local_machine': DllInstall(True, 'hkey_local_machine') spambayes-1.1a6/Outlook2000/config.py0000664000076500000240000005106211116562766017455 0ustar skipstaff00000000000000# configuration classes for the plugin. # We used to use a little pickle, but have since moved to a "spambayes.Options" # class. # Hack for testing - setup sys.path if __name__=='__main__': try: import spambayes.Options except ImportError: import sys, os sys.path.append(os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), ".."))) import sys, types try: _ except NameError: _ = lambda arg: arg FOLDER_ID = r"\(\'[a-fA-F0-9]+\', \'[a-fA-F0-9]+\'\)" FIELD_NAME = r"[a-zA-Z0-9 ]+" # These are stored in the INI file. They must not be localized - we can't # have all option settings being unrecognized just because a new localization # becomes available for users. The dialogs manage this. FILTER_ACTION = "Untouched", "Moved", "Copied" MSG_READ_STATE = "None", "Read", "Unread" from spambayes.OptionsClass import OptionsClass, Option from spambayes.OptionsClass import RESTORE, DO_NOT_RESTORE from spambayes.OptionsClass import BOOLEAN, INTEGER, REAL, PATH, FILE_WITH_PATH class FolderIDOption(Option): def convert(self, value): #print "Convert called on", repr(value) error = None is_multi = self.multiple_values_allowed() # empty string means nothing to single value. if not is_multi and not value: return None # Now sure why we get non-strings here for multis if type(value) == types.ListType: return value # If we really care here, it would be fairly easy to use a regex # etc to pull these IDs apart. eval is easier for now :) try: items = eval(value) except: error = "Invalid value (%s:%s)" % (sys.exc_type, sys.exc_value) check_items = [] if error is None: if is_multi: if type(items) != types.ListType: error = "Multi-valued ID must yield a list" check_items = items else: check_items = [items] if error is None: for item in check_items: if item is None: error = "None isn't valid here (how did it get here anyway?" break if not self.is_valid_single(item): error = "Each ID must be a tuple of 2 strings" break if error is not None: print "Failed to convert FolderID value '%r', is_multi=%d" % \ (value, is_multi) print error if is_multi: return [] else: return None return items def unconvert(self): #print "unconvert called with", repr(self.value) if self.value is None: return "" return str(self.value) def multiple_values_allowed(self): return type(self.value)==types.ListType def is_valid_single(self, value): return value is None or \ (type(value)==types.TupleType and \ len(value)==2 and \ type(value[0])==type(value[1])==types.StringType) defaults = { "General" : ( ("field_score_name", _("The name of the field used to store the spam score"), _("Spam"), _("""SpamBayes stores the spam score for each message in a custom field. This option specifies the name of the field"""), FIELD_NAME, RESTORE), ("data_directory", _("The directory to store the data files."), "", _(""""""), PATH, DO_NOT_RESTORE), ("delete_as_spam_message_state", _("How the 'read' flag on a message is modified"), "None", _("""When the 'Spam' function is used, the message 'read' flag can also be set."""), MSG_READ_STATE, RESTORE), ("recover_from_spam_message_state", _("How the 'read' flag on a message is modified"), "None", _("""When the 'Not Spam' function is used, the message 'read' flag can also be set."""), MSG_READ_STATE, RESTORE), ("verbose", _("Changes the verbosity of the debug output from the program"), 0, _("""Indicates how much information is written to the SpamBayes log file."""), INTEGER, RESTORE), ), # Experimental options may change, may get removed, and *will* get moved # should they be kept. # Experimental options will *never* be exposed via the GUI, meaning that # migrating any such options should be considered a favour :) "Experimental" : ( # These are migrated, so must remain while migration code remains in place. # This isn't critical, so should be deleted after just a few version. ("timer_start_delay", "obsolete", 0, "", INTEGER, RESTORE), ("timer_interval", "obsolete", 1000, "", INTEGER, RESTORE), ("timer_only_receive_folders", "obsolete", True, "", BOOLEAN, RESTORE), ), "Training" : ( (FolderIDOption, "ham_folder_ids", _("Folders containing known good messages"), [], _("""A list of folders known to contain good (ham) messages. When SpamBayes is trained, these messages will be used as examples of good messages."""), FOLDER_ID, DO_NOT_RESTORE), ("ham_include_sub", _("Does the nominated ham folders include sub-folders?"), False, _(""""""), BOOLEAN, DO_NOT_RESTORE), (FolderIDOption, "spam_folder_ids", _("Folders containing known bad or spam messages"), [], _("""A list of folders known to contain bad (spam) messages. When SpamBayes is trained, these messages will be used as examples of messages to filter."""), FOLDER_ID, DO_NOT_RESTORE), ("spam_include_sub", _("Does the nominated spam folders include sub-folders?"), False, _(""""""), BOOLEAN, DO_NOT_RESTORE), ("train_recovered_spam", _("Train as good as items are recovered?"), True, _("""SpamBayes can detect when a message previously classified as spam (or unsure) is moved back to the folder from which it was filtered. If this option is enabled, SpamBayes will automatically train on such messages"""), BOOLEAN, RESTORE), ("train_manual_spam", _("Train as spam items are manually moved?"), True, _("""SpamBayes can detect when a message previously classified as good (or unsure) is manually moved to the Spam folder. If this option is enabled, SpamBayes will automatically train on such messages"""), BOOLEAN, RESTORE), ("rescore", _("Rescore message after training?"), True, _("""After the training has completed, should all the messages be scored for their Spam value. This is particularly useful after your initial training runs, so you can see how effective your sorting of spam and ham was."""), BOOLEAN, RESTORE), ("rebuild", _("Rescore message after training?"), True, _("""Should the entire database be rebuilt? If enabled, then all training information is reset, and a complete new database built from the existing messages in your folders. If disabled, then only new messages in the folders that have not previously been trained on will be processed"""), BOOLEAN, RESTORE), ), # These options control how a message is categorized "Filter" : ( ("filter_now", _("State of 'Filter Now' checkbox"), False, _("""Something useful."""), BOOLEAN, RESTORE), ("save_spam_info", _("Save spam score"), True, _("""Should the spam score and other information be saved in each message as it is filtered or scored?"""), BOOLEAN, RESTORE), (FolderIDOption, "watch_folder_ids", _("Folders to watch for new messages"), [], _("""The list of folders SpamBayes will watch for new messages, processing messages as defined by the filters."""), FOLDER_ID, DO_NOT_RESTORE), ("watch_include_sub", _("Does the nominated watch folders include sub-folders?"), False, _(""""""), BOOLEAN, DO_NOT_RESTORE), (FolderIDOption, "spam_folder_id", _("The folder used to track spam"), None, _("""The folder SpamBayes moves or copies spam to."""), FOLDER_ID, DO_NOT_RESTORE), ("spam_threshold", _("The score necessary to be considered 'certain' spam"), 90.0, _("""Any message with a Spam score greater than or equal to this value will be considered spam, and processed accordingly."""), REAL, RESTORE), ("spam_action", _("The action to take for new spam"), FILTER_ACTION[1], _("""The action that should be taken as Spam messages arrive."""), FILTER_ACTION, RESTORE), ("spam_mark_as_read", _("Should filtered spam also be marked as 'read'"), False, _("""Determines if spam messages are marked as 'Read' as they are filtered. This can be set to 'True' if the new-mail folder counts bother you when the only new items are spam. It can be set to 'False' if you use the 'read' state of these messages to determine which items you are yet to review. This option does not affect the new-mail icon in the system tray."""), BOOLEAN, RESTORE), (FolderIDOption, "unsure_folder_id", _("The folder used to track uncertain messages"), None, _("""The folder SpamBayes moves or copies uncertain messages to."""), FOLDER_ID, DO_NOT_RESTORE), ("unsure_threshold", _("The score necessary to be considered 'unsure'"), 15.0, _("""Any message with a Spam score greater than or equal to this value (but less than the spam threshold) will be considered spam, and processed accordingly."""), REAL, RESTORE), ("unsure_action", _("The action to take for new uncertain messages"), FILTER_ACTION[1], _("""The action that should be taken as unsure messages arrive."""), FILTER_ACTION, RESTORE), ("unsure_mark_as_read", _("Should filtered uncertain message also be marked as 'read'"), False, _("""Determines if unsure messages are marked as 'Read' as they are filtered. See 'spam_mark_as_read' for more details."""), BOOLEAN, RESTORE), (FolderIDOption, "ham_folder_id", _("The folder to which good messages are moved"), None, _("""The folder SpamBayes moves or copies good messages to."""), FOLDER_ID, DO_NOT_RESTORE), ("ham_action", _("The action to take for new good messages"), FILTER_ACTION[0], _("""The action that should be taken as good messages arrive."""), FILTER_ACTION, RESTORE), ("ham_mark_as_read", _("Should filtered good message also be marked as 'read'"), False, _("""Determines if good messages are marked as 'Read' as they are filtered. See 'spam_mark_as_read' for more details."""), BOOLEAN, RESTORE), ("enabled", _("Is filtering enabled?"), False, _(""""""), BOOLEAN, RESTORE), # Options that allow the filtering to be done by a timer. ("timer_enabled", _("Should items be filtered by a timer?"), True, _("""Depending on a number of factors, SpamBayes may occasionally miss messages or conflict with builtin Outlook rules. If this option is set, SpamBayes will filter all messages in the background. This generally solves both of these problem, at the cost of having Spam stay in your inbox for a few extra seconds."""), BOOLEAN, RESTORE), ("timer_start_delay", _("The interval (in seconds) before the timer starts."), 2.0, _("""Once a new item is received in the inbox, SpamBayes will begin processing messages after the given delay. If a new message arrives during this period, the timer will be reset and the delay will start again."""), REAL, RESTORE), ("timer_interval", _("The interval between subsequent timer checks (in seconds)"), 1.0, _("""Once the new message timer finds a new message, how long should SpamBayes wait before checking for another new message, assuming no other new messages arrive. Should a new message arrive during this process, the timer will reset, meaning that timer_start_delay will elapse before the process begins again."""), REAL, RESTORE), ("timer_only_receive_folders", _("Should the timer only be used for 'Inbox' type folders?"), True, _("""The point of using a timer is to prevent the SpamBayes filter getting in the way the builtin Outlook rules. Therefore, is it generally only necessary to use a timer for folders that have new items being delivered directly to them. Folders that are not inbox style folders generally are not subject to builtin filtering, so generally have no problems filtering messages in 'real time'."""), BOOLEAN, RESTORE), ), "Filter_Now": ( (FolderIDOption, "folder_ids", _("Folders to filter in a 'Filter Now' operation"), [], _("""The list of folders that will be filtered by this process."""), FOLDER_ID, DO_NOT_RESTORE), ("include_sub", _("Does the nominated folders include sub-folders?"), False, _(""""""), BOOLEAN, DO_NOT_RESTORE), ("only_unread", _("Only filter unread messages?"), False, _("""When scoring messages, should only messages that are unread be considered?"""), BOOLEAN, RESTORE), ("only_unseen", _("Only filter previously unseen ?"), False, _("""When scoring messages, should only messages that have never previously Spam scored be considered?"""), BOOLEAN, RESTORE), ("action_all", _("Perform all filter actions?"), True, _("""When scoring the messages, should all items be performed (such as moving the items based on the score) or should the items only be scored, but otherwise untouched."""), BOOLEAN, RESTORE), ), # These options control how the user is notified of new messages. "Notification": ( ("notify_sound_enabled", _("Play a notification sound when new messages arrive?"), False, _("""If enabled, SpamBayes will play a notification sound after a batch of new messages is processed. A different sound can be assigned to each of the three classifications of messages. The good sound will be played if any good messages are received. The unsure sound will be played if unsure messages are received, but no good messages. The spam sound will be played if all received messages are spam."""), BOOLEAN, RESTORE), ("notify_ham_sound", _("Sound file to play for good messages"), "", _("""Specifies the full path to a Windows sound file (WAV format) that will be played as notification that a good message has been received."""), FILE_WITH_PATH, DO_NOT_RESTORE), ("notify_unsure_sound", _("Sound file to play for possible spam messages"), "", _("""Specifies the full path to a Windows sound file (WAV format) that will be played as notification that a possible spam message has been received. The unsure notification sound will only be played if no good messages have been received."""), FILE_WITH_PATH, DO_NOT_RESTORE), ("notify_spam_sound", _("Sound file to play for spam messages"), "", _("""Specifies the full path to a Windows sound file (WAV format) that will be played as notification that a spam message has been received. The spam notification sound will only be played if no good or unsure messages have been received."""), FILE_WITH_PATH, DO_NOT_RESTORE), ("notify_accumulate_delay", _("The delay time to wait for additional received messages (in seconds)"), 10.0, _("""When SpamBayes classifies a new message, it sets a timer to wait for additional new messages. If another new message is received before the timer expires then the delay time is reset and SpamBayes continues to wait. If no new messages arrive within the delay time then SpamBayes will play the appropriate notification sound for the received messages."""), REAL, RESTORE), ), } # A simple container that provides "." access to items class SectionContainer: def __init__(self, options, section): self.__dict__['_options'] = options self.__dict__['_section'] = section def __getattr__(self, attr): return self._options.get(self._section, attr) def __setattr__(self, attr, val): return self._options.set(self._section, attr, val) class OptionsContainer: def __init__(self, options): self.__dict__['_options'] = options def __getattr__(self, attr): attr = attr.lower() for key in self._options.sections(): if attr == key.lower(): container = SectionContainer(self._options, key) self.__dict__[attr] = container return container raise AttributeError, "Options has no section '%s'" % attr def __setattr__(self, attr, val): raise AttributeError, "No section [%s]" % attr # and delegate a few methods so this object can be used in place of # a real options object. maybe should add this to getattr. do we want all? def get_option(self, section, name): return self._options.get_option(section, name) def CreateConfig(defaults=defaults): options = OptionsClass() options.load_defaults(defaults) return options def MigrateOptions(options): # Migrate some "old" options to "new" options. Can be deleted in # a few versions :) pass # Old code when we used a pickle. Still needed so old pickles can be # loaded, and moved to the new options file format. class _ConfigurationContainer: def __init__(self, **kw): self.__dict__.update(kw) def __setstate__(self, state): self.__dict__.update(state) def _dump(self, thisname="", level=0): import pprint prefix = " " * level print "%s%s:" % (prefix, thisname) for name, ob in self.__dict__.items(): d = getattr(ob, "_dump", None) if d is None: print "%s %s: %s" % (prefix, name, pprint.pformat(ob)) else: d(name, level+1) class ConfigurationRoot(_ConfigurationContainer): def __init__(self): pass # End of old pickle code. if __name__=='__main__': options = CreateConfig() options.merge_files(['delme.cfg']) c = OptionsContainer(options) f = options.get("Training", "ham_folder_ids") print "Folders before set are", f for i in f: print i, type(i) new_folder_ids = [('000123','456789'), ('ABCDEF', 'FEDCBA')] options.set("Training", "ham_folder_ids", new_folder_ids) f = options.get("Training", "ham_folder_ids") print "Folders after set are", f for i in f: print i, type(i) try: c.filter.oops = "Foo" except (AttributeError,KeyError): # whatever :) pass else: print "ERROR: I was able to set an invalid sub-property!" try: c.oops = "Foo" except (AttributeError,KeyError): # whatever :) pass else: print "ERROR: I was able to set an invalid top-level property!" # Test single ID folders. if c.filter.unsure_folder_id is not None: print "It appears we loaded a folder ID - resetting" c.filter.unsure_folder_id = None unsure_id = c.filter.unsure_folder_id if unsure_id is not None: raise ValueError, "unsure_id wrong (%r)" % (c.filter.unsure_folder_id,) unsure_id = c.filter.unsure_folder_id = ('12345', 'abcdef') if unsure_id != c.filter.unsure_folder_id: raise ValueError, "unsure_id wrong (%r)" % (c.filter.unsure_folder_id,) c.filter.unsure_folder_id = None if c.filter.unsure_folder_id is not None: raise ValueError, "unsure_id wrong (%r)" % (c.filter.unsure_folder_id,) options.set("Filter", "filter_now", True) print "Filter_now from container is", c.filter.filter_now options.set("Filter", "filter_now", False) print "Filter_now from container is now", c.filter.filter_now c.filter.filter_now = True print "Filter_now from container is finally", c.filter.filter_now print "Only unread is", c.filter_now.only_unread v = r"/foo/bar" c.general.data_directory=v if c.general.data_directory!=v: print "Bad directory!", c.general.data_directory v = r"c:\test directory\some sub directory" c.general.data_directory=v if c.general.data_directory!=v: print "Bad directory!", c.general.data_directory v = r"\\server\c$" c.general.data_directory=v if c.general.data_directory!=v: print "Bad directory!", c.general.data_directory options.update_file("delme.cfg") print "Created 'delme.cfg'" spambayes-1.1a6/Outlook2000/config_wizard.py0000664000076500000240000001410010646440136021016 0ustar skipstaff00000000000000# not sure where this should go yet. import config import copy import os # NOTE: The Wizard works from a *complete* copy of the standard options # but with an extra "Wizard" section to maintain state etc for the wizard. # This initial option set may or may not have had values copied from the # real runtime config - this allows either a "re-configure" or a # "clean configure". # Thus, the Wizard still uses standard config option where suitable - eg # filter.watch_folder_ids wizard_defaults = { "Wizard" : ( ("preparation", "How prepared? radio on welcome", 0, """""", config.INTEGER, config.RESTORE), ("need_train", "Will moving to the train page actually train?", True, """""", config.BOOLEAN, config.RESTORE), ("will_train_later", "The user opted to cancel and train later", False, """""", config.BOOLEAN, config.RESTORE), # Spam ("spam_folder_name", "Name of spam folder - ignored if ID set", "Junk E-Mail", """""", "", config.RESTORE), # unsure ("unsure_folder_name", "Name of unsure folder - ignored if ID set", "Junk Suspects", """""", "", config.RESTORE), ("temp_training_names", "", [], "", "", config.RESTORE), ), } def InitWizardConfig(manager, new_config, from_existing): manager.wizard_classifier_data = None # this is hacky new_config.filter.watch_folder_ids = [] new_config.filter.watch_include_sub = False wc = new_config.wizard if from_existing: ids = copy.copy(manager.config.filter.watch_folder_ids) for id in ids: # Only get the folders that actually exist. try: manager.message_store.GetFolder(id) # if we get here, it exists! new_config.filter.watch_folder_ids.append(id) except manager.message_store.MsgStoreException: pass if not new_config.filter.watch_folder_ids: for folder in manager.message_store.YieldReceiveFolders(): new_config.filter.watch_folder_ids.append(folder.GetID()) if from_existing: fc = manager.config.filter if fc.spam_folder_id: try: folder = manager.message_store.GetFolder(fc.spam_folder_id) new_config.filter.spam_folder_id = folder.GetID() wc.spam_folder_name = "" except manager.message_store.MsgStoreException: pass if fc.unsure_folder_id: try: folder = manager.message_store.GetFolder(fc.unsure_folder_id) new_config.filter.unsure_folder_id = folder.GetID() wc.unsure_folder_name = "" except manager.message_store.MsgStoreException: pass tc = manager.config.training if tc.ham_folder_ids: new_config.training.ham_folder_ids = tc.ham_folder_ids if tc.spam_folder_ids: new_config.training.spam_folder_ids = tc.spam_folder_ids if new_config.training.ham_folder_ids or new_config.training.spam_folder_ids: wc.preparation = 1 # "already prepared" def _CreateFolder(manager, name, comment): try: root = manager.message_store.GetRootFolder() new_folder = root.CreateFolder(name, comment, open_if_exists = True) return new_folder except: msg = _("There was an error creating the folder named '%s'\r\n" \ "Please restart Outlook and try again") % name manager.ReportError(msg) return None def CommitWizardConfig(manager, wc): # If the user want to manually configure, then don't do anything if wc.wizard.preparation == 2: # manually configure import dialogs dialogs.ShowDialog(0, manager, manager.config, "IDD_MANAGER") manager.SaveConfig() return # Create the ham and spam folders, if necessary. manager.config.filter.watch_folder_ids = wc.filter.watch_folder_ids if wc.filter.spam_folder_id: manager.config.filter.spam_folder_id = wc.filter.spam_folder_id else: assert wc.wizard.spam_folder_name, "No ID, and no name!!!" f = _CreateFolder(manager, wc.wizard.spam_folder_name, "contains spam filtered by SpamBayes") manager.config.filter.spam_folder_id = f.GetID() if wc.filter.unsure_folder_id: manager.config.filter.unsure_folder_id = wc.filter.unsure_folder_id else: assert wc.wizard.unsure_folder_name, "No ID, and no name!!!" f = _CreateFolder(manager, wc.wizard.unsure_folder_name, "contains messages SpamBayes is uncertain about") manager.config.filter.unsure_folder_id = f.GetID() if wc.training.ham_folder_ids: manager.config.training.ham_folder_ids = wc.training.ham_folder_ids if wc.training.spam_folder_ids: manager.config.training.spam_folder_ids = wc.training.spam_folder_ids wiz_cd = manager.wizard_classifier_data manager.wizard_classifier_data = None if wiz_cd: manager.classifier_data.Adopt(wiz_cd) if wc.wizard.will_train_later: # User cancelled, but said they will sort their mail for us. # don't save the config - this will force the wizard up next time # outlook is started. pass else: # All done - enable, and save the config manager.config.filter.enabled = True manager.SaveConfig() def CancelWizardConfig(manager, wc): if manager.wizard_classifier_data: manager.wizard_classifier_data.Close() manager.wizard_classifier_data = None # Cleanup temp files that may have been created. for fname in wc.wizard.temp_training_names: if os.path.exists(fname): try: os.remove(fname) except OSError: print "Warning: unable to remove", fname def CreateWizardConfig(manager, from_existing): import config defaults = wizard_defaults.copy() defaults.update(config.defaults) options = config.CreateConfig(defaults) cfg = config.OptionsContainer(options) InitWizardConfig(manager, cfg, from_existing) return cfg spambayes-1.1a6/Outlook2000/default_bayes_customize.ini0000664000076500000240000000405310646440136023237 0ustar skipstaff00000000000000# This is the INI file for the *Bayes Engine* as used by the Outlook addin. # It is NOT where configuration information is stored for the addin - see # "[Profile Name].ini" for these settings. # Only options which Outlook requires different from the SpamBayes # defaults are listed here. Thus, there will be very few in the installed # version, but you may list any configuration option valid for the SpamBayes # engine in this file. Again, these are different from the Outlook addin # configuration options. # This file exists in the SpamBayes program directory, and may optionally # exist in the SpamBayes data directory. If you wish to make changes to # the default options, setting the option in a file in your data directory # will persist even when SpamBayes is upgraded. If you change the version in # the SpamBayes program directory, it will be upgraded along with SpamBayes, # so your changes will be lost. Options in the data directory file have # precedence over the app directory file. # Note that versions 0.81 and earlier of the plugin copied this file # to your data directory automatically. If a file with this name exists in # your data directory and you aren't sure you want it, you may delete it. [Tokenizer] # This non-default option is very effective # at nailing Asian spam with little training and small database burden. # It should probably be exposed via the GUI, as it's not appropriate # for people who get "high-bit ham". Asian spam is nailed with this # False too, but it requires more training and a larger database, since # a sufficient variety of "8bit%" and "skip" metatokens take longer to # learn about than strings of question marks. replace_nonascii_chars: True # It's helpful for Tim . record_header_absence: True # Enable basic image cracking - the Windows binaries ship with a gocr binary crack_images: True # For markh, image_size seems maginally helpful for images that the OCR # engine fails to find any text in (the simple fact a .gif is attached is # similarly helpful, but we stick with it for now) image_size: True spambayes-1.1a6/Outlook2000/dialogs/0000775000076500000240000000000011355064626017251 5ustar skipstaff00000000000000spambayes-1.1a6/Outlook2000/dialogs/__init__.py0000664000076500000240000000655010646440134021362 0ustar skipstaff00000000000000# This package defines dialog boxes used by the main # SpamBayes Outlook 2k integration code. import os, sys, stat def LoadDialogs(rc_name = "dialogs.rc"): base_name = os.path.splitext(rc_name)[0] mod_name = "dialogs.resources." + base_name # I18N # Loads a foreign language dialogs.py file, assuming that sys.path # already points to one with the foreign language resources. try: mod = __import__("i18n_" + base_name) except ImportError: mod = None # If we are running from source code, check the .py file is up to date # wrt the .rc file passed in. # If we are running from binaries, the rc name is not used at all - we # assume someone running from source previously generated the .py! if not hasattr(sys, "frozen") and not mod: from resources import rc2py rc_path = os.path.dirname( rc2py.__file__ ) if not os.path.isabs(rc_name): rc_name = os.path.join( rc_path, rc_name) py_name = os.path.join(rc_path, base_name + ".py") mtime = size = None if os.path.exists(py_name): try: mod = __import__(mod_name) mod = sys.modules[mod_name] mtime = mod._rc_mtime_ size = mod._rc_size_ except (ImportError, AttributeError): mtime = None try: stat_data = os.stat(rc_name) rc_mtime = stat_data[stat.ST_MTIME] rc_size = stat_data[stat.ST_SIZE] except OSError: rc_mtime = rc_size = None if rc_mtime!=mtime or rc_size!=size: # Need to generate the dialog. print "Generating %s from %s" % (py_name, rc_name) rc2py.convert(rc_name, py_name) if mod is not None: reload(mod) if mod is None: mod = __import__(mod_name) mod = sys.modules[mod_name] return mod.FakeParser() def ShowDialog(parent, manager, config, idd): """Displays another dialog""" if manager.dialog_parser is None: manager.dialog_parser = LoadDialogs() import dialog_map commands = dialog_map.dialog_map[idd] if not parent: import win32gui try: parent = win32gui.GetActiveWindow() except win32gui.error: pass import dlgcore dlg = dlgcore.ProcessorDialog(parent, manager, config, idd, commands) return dlg.DoModal() def ShowWizard(parent, manager, idd = "IDD_WIZARD", use_existing_config = True): import config_wizard, win32con config = config_wizard.CreateWizardConfig(manager, use_existing_config) if ShowDialog(parent, manager, config, idd) == win32con.IDOK: print "Saving wizard changes" config_wizard.CommitWizardConfig(manager, config) else: print "Cancelling wizard" config_wizard.CancelWizardConfig(manager, config) def MakePropertyPage(parent, manager, config, idd, yoffset=24): """Creates a child dialog box to use as property page in a tab control""" if manager.dialog_parser is None: manager.dialog_parser = LoadDialogs() import dialog_map commands = dialog_map.dialog_map[idd] if not parent: raise "Parent must be the tab control" import dlgcore dlg = dlgcore.ProcessorPage(parent, manager, config, idd, commands, yoffset) return dlg import dlgutils SetWaitCursor = dlgutils.SetWaitCursor spambayes-1.1a6/Outlook2000/dialogs/async_processor.py0000664000076500000240000002515411116562771023045 0ustar skipstaff00000000000000# An async command processor from dlgutils import * import win32gui, win32api, win32con, commctrl import win32process import time import processors verbose = 0 IDC_START = 1100 IDC_PROGRESS = 1101 IDC_PROGRESS_TEXT = 1102 MYWM_SETSTATUS = win32con.WM_USER+11 MYWM_SETWARNING = win32con.WM_USER+12 MYWM_SETERROR = win32con.WM_USER+13 MYWM_FINISHED = win32con.WM_USER+14 # This is called from another thread - hence we need to jump through hoops! class _Progress: def __init__(self, processor): self.hdlg = processor.window.hwnd self.hprogress = processor.GetControl(processor.statusbar_id) self.processor = processor self.stopping = False self.total_control_ticks = 40 self.current_stage = 0 self.set_stages( (("", 1.0),) ) def set_stages(self, stages): self.stages = [] start_pos = 0.0 for name, prop in stages: stage = name, start_pos, prop start_pos += prop self.stages.append(stage) assert abs(start_pos-1.0) < 0.001, ( "Proportions must add to 1.0 (%r,%r,%r)" % (start_pos, stages, start_pos-1.0)) def _next_stage(self): if self.current_stage == 0: win32api.PostMessage(self.hprogress, commctrl.PBM_SETRANGE, 0, MAKELPARAM(0,self.total_control_ticks)) win32api.PostMessage(self.hprogress, commctrl.PBM_SETSTEP, 1, 0) win32api.PostMessage(self.hprogress, commctrl.PBM_SETPOS, 0, 0) self.current_stage += 1 assert self.current_stage <= len(self.stages) def _get_current_stage(self): return self.stages[self.current_stage-1] def set_max_ticks(self, m): # skip to the stage. self._next_stage() self.current_stage_max = m self.current_stage_tick = -1 # ready to go to zero! # if earlier stages stopped early, skip ahead. self.tick() def tick(self): if self.current_stage_tick < self.current_stage_max: # Don't let us go beyond our stage max self.current_stage_tick += 1 # Calc how far through this stage. this_prop = float(self.current_stage_tick) / self.current_stage_max # How far through the total. stage_name, start, end = self._get_current_stage() # Calc the perc of the total control. stage_name, start, prop = self._get_current_stage() total_prop = start + this_prop * prop # How may ticks is this on the control (but always have 1, so the # user knows the process has actually started.) control_tick = max(1,int(total_prop * self.total_control_ticks)) if verbose: print "Tick", self.current_stage_tick, "is", this_prop, "through the stage,", total_prop, "through the total - ctrl tick is", control_tick win32api.PostMessage(self.hprogress, commctrl.PBM_SETPOS, control_tick) def _get_stage_text(self, text): stage_name, start, end = self._get_current_stage() if stage_name: text = stage_name + ": " + text return text def set_status(self, text): self.processor.progress_status = self._get_stage_text(text) win32api.PostMessage(self.hdlg, MYWM_SETSTATUS) def warning(self, text): self.processor.progress_warning = self._get_stage_text(text) win32api.PostMessage(self.hdlg, MYWM_SETWARNING) def error(self, text): self.processor.progress_error = self._get_stage_text(text) win32api.PostMessage(self.hdlg, MYWM_SETERROR) def request_stop(self): self.stopping = True def stop_requested(self): return self.stopping class AsyncCommandProcessor(processors.CommandButtonProcessor): def __init__(self, window, control_ids, func, start_text, stop_text, disable_ids): processors.CommandButtonProcessor.__init__(self, window, control_ids[:1], func, ()) self.progress_status = "" self.progress_error = "" self.progress_warning = "" self.running = False self.statusbar_id = control_ids[1] self.statustext_id = control_ids[2] self.process_start_text = start_text self.process_stop_text = stop_text dids = self.disable_while_running_ids = [] for id in disable_ids.split(): dids.append(window.manager.dialog_parser.ids[id]) def Init(self): win32gui.ShowWindow(self.GetControl(self.statusbar_id), win32con.SW_HIDE) self.SetStatusText("") def Done(self): if self.running: msg = "You must let the running process finish before closing this window" win32gui.MessageBox(self.window.hwnd, msg, "SpamBayes", win32con.MB_OK | win32con.MB_ICONEXCLAMATION) return not self.running def Term(self): # The Window is dieing! We *must* kill it and wait for it to finish # else bad things happen once the main thread dies before us! if self.running: self.progress.request_stop() i = 0 while self.running: win32gui.PumpWaitingMessages(0,-1) if i % 100 == 0: print "Still waiting for async process to finish..." time.sleep(0.01) i += 1 return True def GetMessages(self): return [MYWM_SETSTATUS, MYWM_SETWARNING, MYWM_SETERROR, MYWM_FINISHED] def SetEnabledStates(self, enabled): for id in self.disable_while_running_ids: win32gui.EnableWindow(self.GetControl(id), enabled) def OnMessage(self, msg, wparam, lparam): if msg == MYWM_SETSTATUS: self.OnProgressStatus(wparam, lparam) elif msg == MYWM_SETWARNING: self.OnProgressWarning(wparam, lparam) elif msg == MYWM_SETERROR: self.OnProgressError(wparam, lparam) elif msg == MYWM_FINISHED: self.OnFinished(wparam, lparam) else: raise RuntimeError, "Not one of my messages??" def OnFinished(self, wparam, lparam): self.seen_finished = True wasCancelled = wparam self.SetEnabledStates(True) if self.process_start_text: win32gui.SendMessage(self.GetControl(), win32con.WM_SETTEXT, 0, self.process_start_text) win32gui.ShowWindow(self.GetControl(self.statusbar_id), win32con.SW_HIDE) if wasCancelled: self.SetStatusText("Cancelled") def SetStatusText(self, text): win32gui.SendMessage(self.GetControl(self.statustext_id), win32con.WM_SETTEXT, 0, text) def OnProgressStatus(self, wparam, lparam): self.SetStatusText(self.progress_status) def OnProgressError(self, wparam, lparam): self.SetStatusText(self.progress_error) win32gui.MessageBox(self.window.hwnd, self.progress_error, "SpamBayes", win32con.MB_OK | win32con.MB_ICONEXCLAMATION) if not self.running and not self.seen_finished: self.OnFinished(0,0) def OnProgressWarning(self, wparam, lparam): pass def OnClicked(self, id): self.StartProcess() def StartProcess(self): if self.running: self.progress.request_stop() else: # Do anything likely to fail before we screw around with the # control states - this way the dialog doesn't look as 'dead' progress=_Progress(self) # Now screw around with the control states, restored when # the thread terminates. self.SetEnabledStates(False) if self.process_stop_text: win32gui.SendMessage(self.GetControl(), win32con.WM_SETTEXT, 0, self.process_stop_text) win32gui.SendMessage(self.GetControl(self.statustext_id), win32con.WM_SETTEXT, 0, "") win32gui.ShowWindow(self.GetControl(self.statusbar_id), win32con.SW_SHOW) # Local function for the thread target that notifies us when finished. def thread_target(h, progress): try: self.progress = progress self.seen_finished = False self.running = True # Drop my thread priority, so outlook can keep repainting # and doing its stuff without getting stressed. import win32process, win32api THREAD_PRIORITY_BELOW_NORMAL=-1 win32process.SetThreadPriority(win32api.GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL) self.func( self.window.manager, self.window.config, progress) finally: try: win32api.PostMessage(h, MYWM_FINISHED, self.progress.stop_requested()) except win32api.error: # Bad window handle - already down. pass self.running = False self.progress = None # back to the program :) import threading t = threading.Thread(target=thread_target, args =(self.window.hwnd, progress)) t.start() if __name__=='__main__': verbose = 1 # Test my "multi-stage" code class HackProgress(_Progress): def __init__(self): # dont use dlg self.hprogress = self.hdlg = 0 self.dlg = None self.stopping = False self.total_control_ticks = 40 self.current_stage = 0 self.set_stages( (("", 1.0),) ) print "Single stage test" p = HackProgress() p.set_max_ticks(10) for i in range(10): p.tick() print "First stage test" p = HackProgress() stages = ("Stage 1", 0.2), ("Stage 2", 0.8) p.set_stages(stages) # Do stage 1 p.set_max_ticks(10) for i in range(10): p.tick() # Do stage 2 p.set_max_ticks(20) for i in range(20): p.tick() print "Second stage test" p = HackProgress() stages = ("Stage 1", 0.9), ("Stage 2", 0.1) p.set_stages(stages) p.set_max_ticks(10) for i in range(7): # do a few less just to check p.tick() p.set_max_ticks(2) for i in range(2): p.tick() print "Third stage test" p = HackProgress() stages = ("Stage 1", 0.9), ("Stage 2", 0.1) p.set_stages(stages) p.set_max_ticks(300) for i in range(313): # do a few more just to check p.tick() p.set_max_ticks(2) for i in range(2): p.tick() print "Done!" spambayes-1.1a6/Outlook2000/dialogs/dialog_map.py0000664000076500000240000006702111116610033021705 0ustar skipstaff00000000000000# This module is part of the spambayes project, which is Copyright 2003 # The Python Software Foundation and is covered by the Python Software # Foundation license. from processors import * from opt_processors import * import wizard_processors as wiz from dialogs import ShowDialog, MakePropertyPage, ShowWizard # "dialog specific" processors: class StatsProcessor(ControlProcessor): def __init__(self, window, control_ids): self.button_id = control_ids[1] self.reset_date_id = control_ids[2] ControlProcessor.__init__(self, window, control_ids) self.stats = self.window.manager.stats def Init(self): text = "\n".join(self.stats.GetStats()) win32gui.SendMessage(self.GetControl(), win32con.WM_SETTEXT, 0, text) date_label = self.GetControl(self.reset_date_id) if self.stats.from_date: from time import localtime, strftime reset_date = localtime(self.stats.from_date) date_string = strftime("%a, %d %b %Y %I:%M:%S %p", reset_date) else: date_string = _("Never") win32gui.SendMessage(date_label, win32con.WM_SETTEXT, 0, date_string) def OnCommand(self, wparam, lparam): id = win32api.LOWORD(wparam) if id == self.button_id: self.ResetStatistics() def GetPopupHelpText(self, idFrom): if idFrom == self.control_id: return _("Displays statistics on mail processed by SpamBayes") elif idFrom == self.button_id: return _("Resets all SpamBayes statistics to zero") elif idFrom == self.reset_date_id: return _("The date and time when the SpamBayes statistics were last reset") def ResetStatistics(self): question = _("This will reset all your saved statistics to zero.\r\n\r\n" \ "Are you sure you wish to reset the statistics?") flags = win32con.MB_ICONQUESTION | win32con.MB_YESNO | win32con.MB_DEFBUTTON2 if win32gui.MessageBox(self.window.hwnd, question, "SpamBayes", flags) == win32con.IDYES: self.stats.Reset() self.stats.ResetTotal(True) self.Init() # update the statistics display class VersionStringProcessor(ControlProcessor): def Init(self): from spambayes.Version import get_current_version import sys v = get_current_version() vstring = v.get_long_version("SpamBayes Outlook Addin") if not hasattr(sys, "frozen"): vstring += _(" from source") win32gui.SendMessage(self.GetControl(), win32con.WM_SETTEXT, 0, vstring) def GetPopupHelpText(self, cid): return _("The version of SpamBayes running") class TrainingStatusProcessor(ControlProcessor): def Init(self): bayes = self.window.manager.classifier_data.bayes nspam = bayes.nspam nham = bayes.nham if nspam > 10 and nham > 10: db_status = _("Database has %d good and %d spam.") % (nham, nspam) db_ratio = nham/float(nspam) big = small = None if db_ratio > 5.0: db_status = _("%s\nWarning: you have much more ham than spam - " \ "SpamBayes works best with approximately even " \ "numbers of ham and spam.") % (db_status, ) elif db_ratio < (1/5.0): db_status = _("%s\nWarning: you have much more spam than ham - " \ "SpamBayes works best with approximately even " \ "numbers of ham and spam.") % (db_status, ) elif nspam > 0 or nham > 0: db_status = _("Database only has %d good and %d spam - you should " \ "consider performing additional training.") % (nham, nspam) else: db_status = _("Database has no training information. SpamBayes " \ "will classify all messages as 'unsure', " \ "ready for you to train.") win32gui.SendMessage(self.GetControl(), win32con.WM_SETTEXT, 0, db_status) class WizardTrainingStatusProcessor(ControlProcessor): def Init(self): bayes = self.window.manager.classifier_data.bayes nspam = bayes.nspam nham = bayes.nham if nspam > 10 and nham > 10: msg = _("SpamBayes has been successfully trained and configured. " \ "You should find the system is immediately effective at " \ "filtering spam.") else: msg = _("SpamBayes has been successfully trained and configured. " \ "However, as the number of messages trained is quite small, " \ "SpamBayes may take some time to become truly effective.") win32gui.SendMessage(self.GetControl(), win32con.WM_SETTEXT, 0, msg) class IntProcessor(OptionControlProcessor): def UpdateControl_FromValue(self): win32gui.SendMessage(self.GetControl(), win32con.WM_SETTEXT, 0, str(self.option.get())) def UpdateValue_FromControl(self): buf_size = 100 buf = win32gui.PyMakeBuffer(buf_size) nchars = win32gui.SendMessage(self.GetControl(), win32con.WM_GETTEXT, buf_size, buf) str_val = buf[:nchars] val = int(str_val) if val < 0 or val > 10: raise ValueError, "Value must be between 0 and 10" self.SetOptionValue(val) def OnCommand(self, wparam, lparam): code = win32api.HIWORD(wparam) if code==win32con.EN_CHANGE: try: self.UpdateValue_FromControl() except ValueError: # They are typing - value may be currently invalid pass class FilterEnableProcessor(BoolButtonProcessor): def OnOptionChanged(self, option): self.Init() def Init(self): BoolButtonProcessor.Init(self) reason = self.window.manager.GetDisabledReason() win32gui.EnableWindow(self.GetControl(), reason is None) def UpdateValue_FromControl(self): check = win32gui.SendMessage(self.GetControl(), win32con.BM_GETCHECK) if check: reason = self.window.manager.GetDisabledReason() if reason is not None: win32gui.SendMessage(self.GetControl(), win32con.BM_SETCHECK, 0) raise ValueError, reason check = not not check # force bool! self.SetOptionValue(check) class FilterStatusProcessor(ControlProcessor): def OnOptionChanged(self, option): self.Init() def Init(self): manager = self.window.manager reason = manager.GetDisabledReason() if reason is not None: win32gui.SendMessage(self.GetControl(), win32con.WM_SETTEXT, 0, reason) return if not manager.config.filter.enabled: status = _("Filtering is disabled. Select 'Enable SpamBayes' to enable.") win32gui.SendMessage(self.GetControl(), win32con.WM_SETTEXT, 0, status) return # ok, enabled and working - put together the status text. config = manager.config.filter certain_spam_name = manager.FormatFolderNames( [config.spam_folder_id], False) if config.unsure_folder_id: unsure_name = manager.FormatFolderNames( [config.unsure_folder_id], False) unsure_text = _("Unsure managed in '%s'") % (unsure_name,) else: unsure_text = _("Unsure messages untouched") if config.ham_folder_id: ham_name = manager.FormatFolderNames( [config.ham_folder_id], False) ham_text = _("Good managed in '%s'") % (ham_name,) else: ham_text = _("Good messages untouched") watch_names = manager.FormatFolderNames( config.watch_folder_ids, config.watch_include_sub) filter_status = _("Watching '%s'.\r\n%s.\r\nSpam managed in '%s'.\r\n%s.") \ % (watch_names, ham_text, certain_spam_name, unsure_text) win32gui.SendMessage(self.GetControl(), win32con.WM_SETTEXT, 0, filter_status) class TabProcessor(ControlProcessor): def __init__(self, window, control_ids, page_ids): ControlProcessor.__init__(self, window, control_ids) self.page_ids = page_ids.split() def Init(self): self.pages = {} self.currentPage = None self.currentPageIndex = -1 self.currentPageHwnd = None for index, page_id in enumerate(self.page_ids): template = self.window.manager.dialog_parser.dialogs[page_id] self.addPage(index, page_id, template[0][0]) self.switchToPage(0) def Done(self): if self.currentPageHwnd is not None: if not self.currentPage.SaveAllControls(): win32gui.SendMessage(self.GetControl(), commctrl.TCM_SETCURSEL, self.currentPageIndex,0) return False return True def OnNotify(self, nmhdr, wparam, lparam): # this does not appear to be in commctrl module selChangedCode = 5177342 code = nmhdr[2] if code==selChangedCode: index = win32gui.SendMessage(self.GetControl(), commctrl.TCM_GETCURSEL, 0,0) if index!=self.currentPageIndex: self.switchToPage(index) def switchToPage(self, index): if self.currentPageHwnd is not None: if not self.currentPage.SaveAllControls(): win32gui.SendMessage(self.GetControl(), commctrl.TCM_SETCURSEL, self.currentPageIndex,0) return 1 win32gui.DestroyWindow(self.currentPageHwnd) self.currentPage = MakePropertyPage(self.GetControl(), self.window.manager, self.window.config, self.pages[index]) self.currentPageHwnd = self.currentPage.CreateWindow() self.currentPageIndex = index return 0 def addPage(self, item, idName, label): format = "iiiiiii" lbuf = win32gui.PyMakeBuffer(len(label)+1) address,l = win32gui.PyGetBufferAddressAndLen(lbuf) win32gui.PySetString(address, label) buf = struct.pack(format, commctrl.TCIF_TEXT, # mask 0, # state 0, # state mask address, 0, #unused 0, #image item ) item = win32gui.SendMessage(self.GetControl(), commctrl.TCM_INSERTITEM, item, buf) self.pages[item] = idName def ShowAbout(window): """Opens the SpamBayes documentation in a browser""" window.manager.ShowHtml("about.html") def ShowTrainingDoc(window): """Opens documentation on SpamBayes training in a browser""" window.manager.ShowHtml("docs/welcome.html") def ShowDataFolder(window): """Uses Windows Explorer to show where SpamBayes data and configuration files are stored """ import os import sys filesystem_encoding = sys.getfilesystemencoding() os.startfile(window.manager.data_directory.encode(filesystem_encoding)) def ShowLog(window): """Opens the log file for the current SpamBayes session """ import sys, os, win32api, win32con if hasattr(sys, "frozen"): # current log always "spambayes1.log" log_name = os.path.join(win32api.GetTempPath(), "spambayes1.log") if not os.path.exists(log_name): window.manager.ReportError(_("The log file for this session can not be located")) else: cmd = 'notepad.exe "%s"' % log_name win32api.WinExec(cmd, win32con.SW_SHOW) else: question = _("As you are running from source-code, viewing the\n" \ "log means executing a Python program. If you already\n" \ "have a viewer running, the output may appear in either.\n\n"\ "Do you want to execute this viewer?") if not window.manager.AskQuestion(question): return # source-code users - fire up win32traceutil.py import win32traceutil # will already be imported py_name = win32traceutil.__file__ if py_name[-1] in 'co': # pyc/pyo py_name = py_name[:-1] # execute the .py file - hope that this will manage to invoke # python.exe for it. If this breaks for you, feel free to send me # a patch :) os.system('start ' + win32api.GetShortPathName(py_name)) def ResetConfig(window): question = _("This will reset all configuration options to their default values\r\n\r\n" \ "It will not reset the folders you have selected, nor your\r\n" \ "training information, but all other options will be reset\r\n" \ "and SpamBayes will need to be re-enabled before it will\r\n" \ "continue filtering.\r\n\r\n" \ "Are you sure you wish to reset all options?") flags = win32con.MB_ICONQUESTION | win32con.MB_YESNO | win32con.MB_DEFBUTTON2 if win32gui.MessageBox(window.hwnd, question, "SpamBayes",flags) == win32con.IDYES: options = window.config._options for sect in options.sections(): for opt_name in options.options_in_section(sect): opt = options.get_option(sect, opt_name) if not opt.no_restore(): assert opt.is_valid(opt.default_value), \ "Resetting '%s' to invalid default %r" % (opt.display_name(), opt.default_value) opt.set(opt.default_value) window.LoadAllControls() class DialogCommand(ButtonProcessor): def __init__(self, window, control_ids, idd): self.idd = idd ButtonProcessor.__init__(self, window, control_ids) def OnClicked(self, id): parent = self.window.hwnd # This form and the other form may "share" options, or at least # depend on others. So we must save the current form back to the # options object, display the new dialog, then reload the current # form from the options object/ self.window.SaveAllControls() ShowDialog(parent, self.window.manager, self.window.config, self.idd) self.window.LoadAllControls() def GetPopupHelpText(self, id): dd = self.window.manager.dialog_parser.dialogs[self.idd] return _("Displays the %s dialog") % dd.caption class HiddenDialogCommand(DialogCommand): def __init__(self, window, control_ids, idd): DialogCommand.__init__(self, window, control_ids, idd) def Init(self): DialogCommand.Init(self) # Hide it win32gui.SetWindowText(self.GetControl(), "") def OnCommand(self, wparam, lparam): pass def OnRButtonUp(self, wparam, lparam): self.OnClicked(0) def GetPopupHelpText(self, id): return _("Nothing to see here.") class ShowWizardCommand(DialogCommand): def OnClicked(self, id): import win32con existing = self.window manager = self.window.manager # Kill the main dialog - but first have to find it! dlg = self.window.hwnd while dlg: style = win32api.GetWindowLong(dlg, win32con.GWL_STYLE) if not style & win32con.WS_CHILD: break dlg = win32gui.GetParent(dlg) else: assert 0, "no parent!" try: parent = win32gui.GetParent(dlg) except win32gui.error: parent = 0 # no parent win32gui.EndDialog(dlg, win32con.IDOK) # And show the wizard. ShowWizard(parent, manager, self.idd, use_existing_config = True) def WizardFinish(mgr, window): print _("Wizard Done!") def WizardTrainer(mgr, config, progress): import os, manager, train bayes_base = os.path.join(mgr.data_directory, "$sbwiz$default_bayes_database") mdb_base = os.path.join(mgr.data_directory, "$sbwiz$default_message_database") fnames = [] for ext in ".pck", ".db": fnames.append(bayes_base+ext) fnames.append(mdb_base+ext) config.wizard.temp_training_names = fnames # determine which db manager to use, and create it. ManagerClass = manager.GetStorageManagerClass() db_manager = ManagerClass(bayes_base, mdb_base) classifier_data = manager.ClassifierData(db_manager, mgr) classifier_data.InitNew() rescore = config.training.rescore if rescore: stages = (_("Training"), .3), (_("Saving"), .1), (_("Scoring"), .6) else: stages = (_("Training"), .9), (_("Saving"), .1) progress.set_stages(stages) train.real_trainer(classifier_data, config, mgr.message_store, progress) # xxx - more hacks - we should pass the classifier data in. orig_classifier_data = mgr.classifier_data mgr.classifier_data = classifier_data # temporary try: progress.tick() if rescore: # Setup the "filter now" config to what we want. now_config = config.filter_now now_config.only_unread = False now_config.only_unseen = False now_config.action_all = False now_config.folder_ids = config.training.ham_folder_ids + \ config.training.spam_folder_ids now_config.include_sub = config.training.ham_include_sub or \ config.training.spam_include_sub import filter filter.filterer(mgr, config, progress) bayes = classifier_data.bayes progress.set_status(_("Completed training with %d spam and %d good messages") \ % (bayes.nspam, bayes.nham)) finally: mgr.wizard_classifier_data = classifier_data mgr.classifier_data = orig_classifier_data from async_processor import AsyncCommandProcessor import filter, train dialog_map = { "IDD_MANAGER" : ( (CloseButtonProcessor, "IDOK IDCANCEL"), (TabProcessor, "IDC_TAB", """IDD_GENERAL IDD_FILTER IDD_TRAINING IDD_STATISTICS IDD_NOTIFICATIONS IDD_ADVANCED"""), (CommandButtonProcessor, "IDC_ABOUT_BTN", ShowAbout, ()), ), "IDD_GENERAL": ( (ImageProcessor, "IDC_LOGO_GRAPHIC"), (VersionStringProcessor, "IDC_VERSION"), (TrainingStatusProcessor, "IDC_TRAINING_STATUS"), (FilterEnableProcessor, "IDC_BUT_FILTER_ENABLE", "Filter.enabled"), (FilterStatusProcessor, "IDC_FILTER_STATUS"), (ShowWizardCommand, "IDC_BUT_WIZARD", "IDD_WIZARD"), (CommandButtonProcessor, "IDC_BUT_RESET", ResetConfig, ()), ), "IDD_FILTER_NOW" : ( (CloseButtonProcessor, "IDCANCEL"), (BoolButtonProcessor, "IDC_BUT_UNREAD", "Filter_Now.only_unread"), (BoolButtonProcessor, "IDC_BUT_UNSEEN", "Filter_Now.only_unseen"), (BoolButtonProcessor, "IDC_BUT_ACT_ALL IDC_BUT_ACT_SCORE", "Filter_Now.action_all"), (FolderIDProcessor, "IDC_FOLDER_NAMES IDC_BROWSE", "Filter_Now.folder_ids", "Filter_Now.include_sub"), (AsyncCommandProcessor, "IDC_START IDC_PROGRESS IDC_PROGRESS_TEXT", filter.filterer, _("Start Filtering"), _("Stop Filtering"), """IDCANCEL IDC_BUT_UNSEEN IDC_BUT_UNREAD IDC_BROWSE IDC_BUT_ACT_SCORE IDC_BUT_ACT_ALL"""), ), "IDD_FILTER" : ( (FolderIDProcessor, "IDC_FOLDER_WATCH IDC_BROWSE_WATCH", "Filter.watch_folder_ids", "Filter.watch_include_sub"), (ComboProcessor, "IDC_ACTION_CERTAIN", "Filter.spam_action", _("Untouched,Moved,Copied")), (FolderIDProcessor, "IDC_FOLDER_CERTAIN IDC_BROWSE_CERTAIN", "Filter.spam_folder_id"), (EditNumberProcessor, "IDC_EDIT_CERTAIN IDC_SLIDER_CERTAIN", "Filter.spam_threshold"), (BoolButtonProcessor, "IDC_MARK_SPAM_AS_READ", "Filter.spam_mark_as_read"), (FolderIDProcessor, "IDC_FOLDER_UNSURE IDC_BROWSE_UNSURE", "Filter.unsure_folder_id"), (EditNumberProcessor, "IDC_EDIT_UNSURE IDC_SLIDER_UNSURE", "Filter.unsure_threshold"), (ComboProcessor, "IDC_ACTION_UNSURE", "Filter.unsure_action", _("Untouched,Moved,Copied")), (BoolButtonProcessor, "IDC_MARK_UNSURE_AS_READ", "Filter.unsure_mark_as_read"), (FolderIDProcessor, "IDC_FOLDER_HAM IDC_BROWSE_HAM", "Filter.ham_folder_id"), (ComboProcessor, "IDC_ACTION_HAM", "Filter.ham_action", _("Untouched,Moved,Copied")), ), "IDD_TRAINING" : ( (FolderIDProcessor, "IDC_STATIC_HAM IDC_BROWSE_HAM", "Training.ham_folder_ids", "Training.ham_include_sub"), (FolderIDProcessor, "IDC_STATIC_SPAM IDC_BROWSE_SPAM", "Training.spam_folder_ids", "Training.spam_include_sub"), (BoolButtonProcessor, "IDC_BUT_RESCORE", "Training.rescore"), (BoolButtonProcessor, "IDC_BUT_REBUILD", "Training.rebuild"), (AsyncCommandProcessor, "IDC_START IDC_PROGRESS IDC_PROGRESS_TEXT", train.trainer, _("Start Training"), _("Stop"), "IDOK IDCANCEL IDC_BROWSE_HAM IDC_BROWSE_SPAM " \ "IDC_BUT_REBUILD IDC_BUT_RESCORE"), (BoolButtonProcessor, "IDC_BUT_TRAIN_FROM_SPAM_FOLDER", "Training.train_recovered_spam"), (BoolButtonProcessor, "IDC_BUT_TRAIN_TO_SPAM_FOLDER", "Training.train_manual_spam"), (ComboProcessor, "IDC_DEL_SPAM_RS", "General.delete_as_spam_message_state", _("not change the message,mark the message as read,mark the message as unread")), (ComboProcessor, "IDC_RECOVER_RS", "General.recover_from_spam_message_state", _("not change the message,mark the message as read,mark the message as unread")), ), "IDD_STATISTICS" : ( (StatsProcessor, "IDC_STATISTICS IDC_BUT_RESET_STATS " \ "IDC_LAST_RESET_DATE"), ), "IDD_NOTIFICATIONS" : ( (BoolButtonProcessor, "IDC_ENABLE_SOUNDS", "Notification.notify_sound_enabled", """IDC_HAM_SOUND IDC_BROWSE_HAM_SOUND IDC_UNSURE_SOUND IDC_BROWSE_UNSURE_SOUND IDC_SPAM_SOUND IDC_BROWSE_SPAM_SOUND IDC_ACCUMULATE_DELAY_SLIDER IDC_ACCUMULATE_DELAY_TEXT"""), (FilenameProcessor, "IDC_HAM_SOUND IDC_BROWSE_HAM_SOUND", "Notification.notify_ham_sound", _("Sound Files (*.wav)|*.wav|All Files (*.*)|*.*")), (FilenameProcessor, "IDC_UNSURE_SOUND IDC_BROWSE_UNSURE_SOUND", "Notification.notify_unsure_sound", _("Sound Files (*.wav)|*.wav|All Files (*.*)|*.*")), (FilenameProcessor, "IDC_SPAM_SOUND IDC_BROWSE_SPAM_SOUND", "Notification.notify_spam_sound", _("Sound Files (*.wav)|*.wav|All Files (*.*)|*.*")), (EditNumberProcessor, "IDC_ACCUMULATE_DELAY_TEXT IDC_ACCUMULATE_DELAY_SLIDER", "Notification.notify_accumulate_delay", 0, 30, 20, 60), ), "IDD_ADVANCED" : ( (BoolButtonProcessor, "IDC_BUT_TIMER_ENABLED", "Filter.timer_enabled", """IDC_DELAY1_TEXT IDC_DELAY1_SLIDER IDC_DELAY2_TEXT IDC_DELAY2_SLIDER IDC_INBOX_TIMER_ONLY"""), (EditNumberProcessor, "IDC_DELAY1_TEXT IDC_DELAY1_SLIDER", "Filter.timer_start_delay", 0, 10, 20, 60), (EditNumberProcessor, "IDC_DELAY2_TEXT IDC_DELAY2_SLIDER", "Filter.timer_interval", 0, 10, 20, 60), (BoolButtonProcessor, "IDC_INBOX_TIMER_ONLY", "Filter.timer_only_receive_folders"), (CommandButtonProcessor, "IDC_SHOW_DATA_FOLDER", ShowDataFolder, ()), (DialogCommand, "IDC_BUT_SHOW_DIAGNOSTICS", "IDD_DIAGNOSTIC"), ), "IDD_DIAGNOSTIC" : ( (BoolButtonProcessor, "IDC_SAVE_SPAM_SCORE", "Filter.save_spam_info"), (IntProcessor, "IDC_VERBOSE_LOG", "General.verbose"), (CommandButtonProcessor, "IDC_BUT_VIEW_LOG", ShowLog, ()), (CloseButtonProcessor, "IDOK IDCANCEL"), ), # All the wizards "IDD_WIZARD": ( (ImageProcessor, "IDC_WIZ_GRAPHIC"), (CloseButtonProcessor, "IDCANCEL"), (wiz.ConfigureWizardProcessor, "IDC_FORWARD_BTN IDC_BACK_BTN IDC_PAGE_PLACEHOLDER", """IDD_WIZARD_WELCOME IDD_WIZARD_FOLDERS_WATCH IDD_WIZARD_FOLDERS_REST IDD_WIZARD_FOLDERS_TRAIN IDD_WIZARD_TRAIN IDD_WIZARD_TRAINING_IS_IMPORTANT IDD_WIZARD_FINISHED_UNCONFIGURED IDD_WIZARD_FINISHED_UNTRAINED IDD_WIZARD_FINISHED_TRAINED IDD_WIZARD_FINISHED_TRAIN_LATER """, WizardFinish), ), "IDD_WIZARD_WELCOME": ( (CommandButtonProcessor, "IDC_BUT_ABOUT", ShowAbout, ()), (RadioButtonProcessor, "IDC_BUT_PREPARATION", "Wizard.preparation"), ), "IDD_WIZARD_TRAINING_IS_IMPORTANT" : ( (BoolButtonProcessor, "IDC_BUT_TRAIN IDC_BUT_UNTRAINED", "Wizard.will_train_later"), (CommandButtonProcessor, "IDC_BUT_ABOUT", ShowTrainingDoc, ()), ), "IDD_WIZARD_FOLDERS_REST": ( (wiz.EditableFolderIDProcessor,"IDC_FOLDER_CERTAIN IDC_BROWSE_SPAM", "Filter.spam_folder_id", "Wizard.spam_folder_name", "Training.spam_folder_ids"), (wiz.EditableFolderIDProcessor,"IDC_FOLDER_UNSURE IDC_BROWSE_UNSURE", "Filter.unsure_folder_id", "Wizard.unsure_folder_name"), ), "IDD_WIZARD_FOLDERS_WATCH": ( (wiz.WatchFolderIDProcessor,"IDC_FOLDER_WATCH IDC_BROWSE_WATCH", "Filter.watch_folder_ids"), ), "IDD_WIZARD_FOLDERS_TRAIN": ( (wiz.TrainFolderIDProcessor,"IDC_FOLDER_HAM IDC_BROWSE_HAM", "Training.ham_folder_ids"), (wiz.TrainFolderIDProcessor,"IDC_FOLDER_CERTAIN IDC_BROWSE_SPAM", "Training.spam_folder_ids"), (BoolButtonProcessor, "IDC_BUT_RESCORE", "Training.rescore"), ), "IDD_WIZARD_TRAIN" : ( (wiz.WizAsyncProcessor, "IDC_PROGRESS IDC_PROGRESS_TEXT", WizardTrainer, "", "", ""), ), "IDD_WIZARD_FINISHED_UNCONFIGURED": ( ), "IDD_WIZARD_FINISHED_UNTRAINED": ( ), "IDD_WIZARD_FINISHED_TRAINED": ( (WizardTrainingStatusProcessor, "IDC_TRAINING_STATUS"), ), "IDD_WIZARD_FINISHED_TRAIN_LATER" : ( ), } spambayes-1.1a6/Outlook2000/dialogs/dlgcore.py0000664000076500000240000003117310646440134021241 0ustar skipstaff00000000000000# A core, data-driven dialog. # Driven completely by "Control Processor" objects. # This module is part of the spambayes project, which is Copyright 2003 # The Python Software Foundation and is covered by the Python Software # Foundation license. import win32gui, win32api, win32con import commctrl import struct, array from dlgutils import * # Isolate the nasty stuff for tooltips somewhere. class TooltipManager: def __init__(self, dialog): self.dialog = dialog self.hwnd_tooltip = None self.tooltip_tools = {} def HideTooltip(self): if self.hwnd_tooltip is not None: win32gui.SendMessage(self.hwnd_tooltip, commctrl.TTM_TRACKACTIVATE, 0, 0) def ShowTooltipForControl(self, control_id, text): # Note sure this tooltip stuff is quite right! # Hide an existing one, so the new one gets created. # (new one empty is no big deal, but hiding the old one is, so # we get re-queried for the text. hwnd_dialog = self.dialog.hwnd self.HideTooltip() if self.hwnd_tooltip is None: TTS_BALLOON = 0x40 self.hwnd_tooltip = win32gui.CreateWindowEx(0, "tooltips_class32", None, win32con.WS_POPUP | TTS_BALLOON, win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT, hwnd_dialog, 0, 0, None) # 80 chars max for our tooltip # hrm - how to measure this in pixels! win32gui.SendMessage(self.hwnd_tooltip, commctrl.TTM_SETMAXTIPWIDTH, 0, 300) format = "iiiiiiiiiii" tt_size = struct.calcsize(format) buffer = array.array("c", text + "\0") text_address, size = buffer.buffer_info() uID = control_id flags = commctrl.TTF_TRACK | commctrl.TTF_ABSOLUTE data = struct.pack(format, tt_size, flags, hwnd_dialog, uID, 0,0,0,0, 0, text_address, 0) # Add a tool for this control only if we haven't already if control_id not in self.tooltip_tools: win32gui.SendMessage(self.hwnd_tooltip, commctrl.TTM_ADDTOOL, 0, data) self.tooltip_tools[control_id] = 1 control = win32gui.GetDlgItem(hwnd_dialog, control_id) child_rect = win32gui.GetWindowRect(control) xOff = yOff = 15 # just below and right of the control win32gui.SendMessage(self.hwnd_tooltip, commctrl.TTM_TRACKPOSITION, 0, MAKELONG(child_rect[0]+xOff, child_rect[1]+yOff)) win32gui.SendMessage(self.hwnd_tooltip, commctrl.TTM_TRACKACTIVATE, 1,data) # A base dialog class, that loads from resources. Has no real smarts. class Dialog: def __init__(self, parent, parser, idd): win32gui.InitCommonControls() self.hinst = win32api.GetModuleHandle(None) self.parent = parent self.dialog_parser = parser self.template = parser.dialogs[idd] def _GetIDName(self, cid): return self.dialog_parser.names.get(cid, str(cid)) def CreateWindow(self): return self._DoCreate(win32gui.CreateDialogIndirect) def DoModal(self): return self._DoCreate(win32gui.DialogBoxIndirect) def GetMessageMap(self): ret = { #win32con.WM_SIZE: self.OnSize, win32con.WM_COMMAND: self.OnCommand, win32con.WM_NOTIFY: self.OnNotify, win32con.WM_INITDIALOG: self.OnInitDialog, win32con.WM_CLOSE: self.OnClose, win32con.WM_DESTROY: self.OnDestroy, win32con.WM_RBUTTONUP: self.OnRButtonUp, } return ret def DoInitialPosition(self): # centre the dialog desktop = win32gui.GetDesktopWindow() l,t,r,b = win32gui.GetWindowRect(self.hwnd) w = r-l h = b-t dt_l, dt_t, dt_r, dt_b = win32gui.GetWindowRect(desktop) centre_x, centre_y = win32gui.ClientToScreen( desktop, ( (dt_r-dt_l)/2, (dt_b-dt_t)/2) ) win32gui.MoveWindow(self.hwnd, centre_x-(w/2), centre_y-(h/2), w, h, 0) def OnInitDialog(self, hwnd, msg, wparam, lparam): self.hwnd = hwnd self.DoInitialPosition() def OnCommand(self, hwnd, msg, wparam, lparam): pass def OnNotify(self, hwnd, msg, wparam, lparam): pass def OnClose(self, hwnd, msg, wparam, lparam): pass def OnDestroy(self, hwnd, msg, wparam, lparam): pass def OnRButtonUp(self, hwnd, msg, wparam, lparam): pass def _DoCreate(self, fn): message_map = self.GetMessageMap() return win32gui.DialogBoxIndirect(self.hinst, self.template, self.parent, message_map) # A couple of helpers def GetDlgItem(self, id): if type(id)==type(''): id = self.dialog_parser.ids[id] return win32gui.GetDlgItem(self.hwnd, id) def SetDlgItemText(self, id, text): hchild = self.GetDlgItem(id) win32gui.SendMessage(hchild, win32con.WM_SETTEXT, 0, text) # A dialog with a tooltip manager class TooltipDialog(Dialog): def __init__(self, parent, parser, idd): Dialog.__init__(self, parent, parser, idd) self.tt = TooltipManager(self) def GetMessageMap(self): ret = Dialog.GetMessageMap(self) ret.update( { win32con.WM_HELP: self.OnHelp, win32con.WM_LBUTTONDOWN: self.OnLButtonDown, win32con.WM_ACTIVATE: self.OnActivate, }) return ret def OnLButtonDown(self, hwnd, msg, wparam, lparam): self.tt.HideTooltip() def OnActivate(self, hwnd, msg, wparam, lparam): self.tt.HideTooltip() def OnDestroy(self, hwnd, msg, wparam, lparam): self.tt.HideTooltip() def OnHelp(self, hwnd, msg, wparam, lparam): format = "iiiiiii" buf = win32gui.PyMakeBuffer(struct.calcsize(format), lparam) cbSize, iContextType, iCtrlId, hItemHandle, dwContextID, x, y = \ struct.unpack(format, buf) #print "OnHelp", cbSize, iContextType, iCtrlId, hItemHandle, dwContextID, x, y tt_text = self.GetPopupHelpText(iCtrlId) if tt_text: self.tt.ShowTooltipForControl(iCtrlId, tt_text) else: self.tt.HideTooltip() return 1 def GetPopupHelpText(self, control_id): return None # A "Processor Dialog" works with Command Processors, to link SpamBayes # options with control IDS, giving a "data driven" dialog. class ProcessorDialog(TooltipDialog): def __init__(self, parent, manager, config, idd, option_handlers): TooltipDialog.__init__(self, parent, manager.dialog_parser, idd) parser = manager.dialog_parser self.manager = manager self.config = config self.command_processors = {} self.processor_message_map = {} # WM_MESSAGE : [processors_who_want_it] self.all_processors = [] for data in option_handlers: klass = data[0] id_names = data[1] rest = data[2:] ids = id_names.split() int_ids = [ parser.ids[id] for id in ids] instance = klass(self,int_ids, *rest) self.all_processors.append(instance) for int_id in int_ids: self.command_processors[int_id] = instance for message in instance.GetMessages(): existing = self.processor_message_map.setdefault(message, []) existing.append(instance) def GetMessageMap(self): ret = TooltipDialog.GetMessageMap(self) for key in self.processor_message_map.keys(): if key in ret: print "*** WARNING: Overwriting message!!!" ret[key] = self.OnCommandProcessorMessage return ret def OnInitDialog(self, hwnd, msg, wparam, lparam): TooltipDialog.OnInitDialog(self, hwnd, msg, wparam, lparam) if __debug__: # this is just a debugging aid for int_id in self.command_processors: try: self.GetDlgItem(int_id) except win32gui.error: print "ERROR: Dialog item %s refers to an invalid control" % \ self._GetIDName(int_id) self.LoadAllControls() def GetPopupHelpText(self, iCtrlId): cp = self.command_processors.get(iCtrlId) tt_text = None if cp is not None: return cp.GetPopupHelpText(iCtrlId) print "Can not get command processor for", self._GetIDName(iCtrlId) return None def OnRButtonUp(self, hwnd, msg, wparam, lparam): for cp in self.command_processors.values(): cp.OnRButtonUp(wparam,lparam) def OnCommandProcessorMessage(self, hwnd, msg, wparam, lparam): for p in self.processor_message_map[msg]: p.OnMessage(msg, wparam, lparam) # Called back by a processor when it changes an option. We tell all other # options on our page that the value changed. def OnOptionChanged(self, changed_by, option): for p in self.all_processors: if p is not changed_by: p.OnOptionChanged(option) def OnDestroy(self, hwnd, msg, wparam, lparam): #print "OnDestroy" for p in self.all_processors: p.Term() TooltipDialog.OnDestroy(self, hwnd, msg, wparam, lparam) self.command_processors = None self.all_processors = None self.processor_message_map = None def LoadAllControls(self): for p in self.all_processors: p.Init() def ApplyHandlingOptionValueError(self, func, *args): try: return func(*args) except ValueError, why: mb_flags = win32con.MB_ICONEXCLAMATION | win32con.MB_OK win32gui.MessageBox(self.hwnd, str(why), "SpamBayes", mb_flags) return False def SaveAllControls(self): for p in self.all_processors: if not self.ApplyHandlingOptionValueError(p.Done): win32gui.SetFocus(p.GetControl()) return False return True def OnClose(self, hwnd, msg, wparam, lparam): if TooltipDialog.OnClose(self, hwnd, msg, wparam, lparam): return 1 if not self.SaveAllControls(): return 1 win32gui.EndDialog(hwnd, 0) def OnNotify(self, hwnd, msg, wparam, lparam): #print "OnNotify", hwnd, msg, wparam, lparam # Parse the NMHDR TooltipDialog.OnNotify(self, hwnd, msg, wparam, lparam) format = "iii" buf = win32gui.PyMakeBuffer(struct.calcsize(format), lparam) hwndFrom, idFrom, code = struct.unpack(format, buf) code += 0x4f0000 # hrm - wtf - commctrl uses this, and it works with mfc. *sigh* # delegate rest to our commands. handler = self.command_processors.get(idFrom) if handler is None: print "Ignoring OnNotify for", self._GetIDName(idFrom) return return handler.OnNotify( (hwndFrom, idFrom, code), wparam, lparam) def OnCommand(self, hwnd, msg, wparam, lparam): TooltipDialog.OnCommand(self, hwnd, msg, wparam, lparam) id = win32api.LOWORD(wparam) # Sometimes called after OnDestroy??? if self.command_processors is None: print "Ignoring OnCommand for", self._GetIDName(id) return else: handler = self.command_processors.get(id) if handler is None: print "Ignoring OnCommand for", self._GetIDName(id) return self.ApplyHandlingOptionValueError(handler.OnCommand, wparam, lparam) class ProcessorPage(ProcessorDialog): def __init__(self, parent, manager, config, idd, option_handlers, yoffset): ProcessorDialog.__init__(self, parent, manager, config, idd,option_handlers) self.yoffset = yoffset def DoInitialPosition(self): # The hardcoded values are a bit of a hack. win32gui.SetWindowPos(self.hwnd, win32con.HWND_TOP, 1, self.yoffset, 0, 0, win32con.SWP_NOSIZE) def CreateWindow(self): # modeless. Pages should have the WS_CHILD window style message_map = self.GetMessageMap() # remove frame from dialog and make sure it is a child self.template[0][2] = self.template[0][2] & ~(win32con.DS_MODALFRAME|win32con.WS_POPUP|win32con.WS_OVERLAPPED|win32con.WS_CAPTION) self.template[0][2] = self.template[0][2] | win32con.WS_CHILD return win32gui.CreateDialogIndirect(self.hinst, self.template, self.parent, message_map) spambayes-1.1a6/Outlook2000/dialogs/dlgutils.py0000664000076500000240000000077710646440134021457 0ustar skipstaff00000000000000# Generic utilities for dialog functions. # This module is part of the spambayes project, which is Copyright 2002 # The Python Software Foundation and is covered by the Python Software # Foundation license. def MAKELONG(l,h): return ((h & 0xFFFF) << 16) | (l & 0xFFFF) MAKELPARAM=MAKELONG def SetWaitCursor(wait): import win32gui, win32con if wait: hCursor = win32gui.LoadCursor(0, win32con.IDC_WAIT) else: hCursor = win32gui.LoadCursor(0, 0) win32gui.SetCursor(hCursor) spambayes-1.1a6/Outlook2000/dialogs/FolderSelector.py0000664000076500000240000010417311116562775022550 0ustar skipstaff00000000000000from __future__ import generators import sys, os import win32con import commctrl import win32api import win32gui import winerror import struct, array import dlgutils from pprint import pprint # debugging only verbose = 0 def INDEXTOSTATEIMAGEMASK(i): # from new commctrl.h return i << 12 IIL_UNCHECKED = 1 IIL_CHECKED = 2 # Helpers for building the folder list class FolderSpec: def __init__(self, folder_id, name, ignore_eids = None): self.folder_id = folder_id self.name = name self.children = [] self.ignore_eids = ignore_eids def dump(self, level=0): prefix = " " * level print prefix + self.name for c in self.children: c.dump(level+1) # Oh, lord help us. # We started with a CDO version - but CDO sucks for lots of reasons I # wont even start to mention. # So we moved to an Extended MAPI version with is nice and fast - screams # along! Except it doesn't work in all cases with Exchange (which # strikes Mark as extremely strange given that the Extended MAPI Python # bindings were developed against an Exchange Server - but Mark doesn't # have an Exchange server handy these days, and really doesn't give a # rat's arse ). # So finally we have an Outlook object model version! # But then Tony Meyer came to the rescue - he noticed that we were # simply using short-term EID values for Exchange Folders - so now that # is solved, we are back to the Extended MAPI version. # These variants were deleted by MarkH - cvs is your friend :) # Last appeared in Rev 1.10 ######################################################################### ## An extended MAPI version ######################################################################### from win32com.mapi import mapi, mapiutil from win32com.mapi.mapitags import * import pythoncom def _BuildFoldersMAPI(manager, folder_spec): # This is called dynamically as folders are expanded. dlgutils.SetWaitCursor(1) folder = manager.message_store.GetFolder(folder_spec.folder_id).OpenEntry() # Get the hierarchy table for it. table = folder.GetHierarchyTable(0) children = [] order = (((PR_DISPLAY_NAME_A, mapi.TABLE_SORT_ASCEND),),0,0) rows = mapi.HrQueryAllRows(table, (PR_ENTRYID, PR_STORE_ENTRYID, PR_DISPLAY_NAME_A), None, order, 0) if verbose: print "Rows for sub-folder of", folder_spec.name, "-", folder_spec.folder_id pprint(rows) for (eid_tag, eid),(storeeid_tag, store_eid), (name_tag, name) in rows: # Note the eid we get here is short-term - hence we must # re-fetch from the object itself (which is what our manager does, # so no need to do it explicitly - just believe folder.id over eid) ignore = False for check_eid in folder_spec.ignore_eids: if manager.message_store.session.CompareEntryIDs(check_eid, eid): ignore = True break if ignore: continue temp_id = mapi.HexFromBin(store_eid), mapi.HexFromBin(eid) try: # may get MsgStoreException for GetFolder, or # a mapi exception for the underlying MAPI stuff we then call. # Either way, just skip it. child_folder = manager.message_store.GetFolder(temp_id) spec = FolderSpec(child_folder.GetID(), name, folder_spec.ignore_eids) # If we have no children at all, indicate # the item is not expandable. table = child_folder.OpenEntry().GetHierarchyTable(0) if table.GetRowCount(0) == 0: spec.children = [] else: spec.children = None # Flag as "not yet built" children.append(spec) except (pythoncom.com_error, manager.message_store.MsgStoreException), details: # Users have reported failure here - it is not clear if the # entire tree is going to fail, or just this folder print "** Unable to open child folder - ignoring" print details dlgutils.SetWaitCursor(0) return children def BuildFolderTreeMAPI(session, ignore_ids): root = FolderSpec(None, "root") tab = session.GetMsgStoresTable(0) prop_tags = PR_ENTRYID, PR_DISPLAY_NAME_A rows = mapi.HrQueryAllRows(tab, prop_tags, None, None, 0) if verbose: print "message store rows:" pprint(rows) for row in rows: (eid_tag, eid), (name_tag, name) = row hex_eid = mapi.HexFromBin(eid) try: msgstore = session.OpenMsgStore(0, eid, None, mapi.MDB_NO_MAIL | mapi.MAPI_DEFERRED_ERRORS) hr, data = msgstore.GetProps((PR_IPM_SUBTREE_ENTRYID,)+ignore_ids, 0) # It appears that not all stores have a subtree. if PROP_TYPE(data[0][0]) != PT_BINARY: print "FolderSelector dialog found message store without a subtree - ignoring" continue subtree_eid = data[0][1] ignore_eids = [item[1] for item in data[1:] if PROP_TYPE(item[0])==PT_BINARY] except pythoncom.com_error, details: # Handle 'expected' errors. if details[0]== mapi.MAPI_E_FAILONEPROVIDER: print "A message store is temporarily unavailable - " \ "it will not appear in the Folder Selector dialog" else: # Some weird error opening a folder tree # Just print a warning and ignore the tree. print "Failed to open a message store for the FolderSelector dialog" print "Exception details:", details continue folder_id = hex_eid, mapi.HexFromBin(subtree_eid) if verbose: print "message store root folder id is", folder_id spec = FolderSpec(folder_id, name, ignore_eids) spec.children = None root.children.append(spec) return root # XXX - Note - the following structure code has been copied into the new # XXX - win32gui_struct module. One day we can rip this in preference # XXX - for this new standard win32all module # Helpers for the ugly win32 structure packing/unpacking def _GetMaskAndVal(val, default, mask, flag): if val is None: return mask, default else: mask |= flag return mask, val def PackTVINSERTSTRUCT(parent, insertAfter, tvitem): tvitem_buf, extra = PackTVITEM(*tvitem) tvitem_buf = tvitem_buf.tostring() format = "ii%ds" % len(tvitem_buf) return struct.pack(format, parent, insertAfter, tvitem_buf), extra def PackTVITEM(hitem, state, stateMask, text, image, selimage, citems, param): extra = [] # objects we must keep references to mask = 0 mask, hitem = _GetMaskAndVal(hitem, 0, mask, commctrl.TVIF_HANDLE) mask, state = _GetMaskAndVal(state, 0, mask, commctrl.TVIF_STATE) if not mask & commctrl.TVIF_STATE: stateMask = 0 mask, text = _GetMaskAndVal(text, None, mask, commctrl.TVIF_TEXT) mask, image = _GetMaskAndVal(image, 0, mask, commctrl.TVIF_IMAGE) mask, selimage = _GetMaskAndVal(selimage, 0, mask, commctrl.TVIF_SELECTEDIMAGE) mask, citems = _GetMaskAndVal(citems, 0, mask, commctrl.TVIF_CHILDREN) mask, param = _GetMaskAndVal(param, 0, mask, commctrl.TVIF_PARAM) if text is None: text_addr = text_len = 0 else: text_buffer = array.array("c", text+"\0") extra.append(text_buffer) text_addr, text_len = text_buffer.buffer_info() format = "iiiiiiiiii" buf = struct.pack(format, mask, hitem, state, stateMask, text_addr, text_len, # text image, selimage, citems, param) return array.array("c", buf), extra # Make a new buffer suitable for querying hitem's attributes. def EmptyTVITEM(hitem, mask = None, text_buf_size=512): extra = [] # objects we must keep references to if mask is None: mask = commctrl.TVIF_HANDLE | commctrl.TVIF_STATE | commctrl.TVIF_TEXT | \ commctrl.TVIF_IMAGE | commctrl.TVIF_SELECTEDIMAGE | \ commctrl.TVIF_CHILDREN | commctrl.TVIF_PARAM if mask & commctrl.TVIF_TEXT: text_buffer = array.array("c", "\0" * text_buf_size) extra.append(text_buffer) text_addr, text_len = text_buffer.buffer_info() else: text_addr = text_len = 0 format = "iiiiiiiiii" buf = struct.pack(format, mask, hitem, 0, 0, text_addr, text_len, # text 0, 0, 0, 0) return array.array("c", buf), extra def UnpackTVItem(buffer): item_mask, item_hItem, item_state, item_stateMask, \ item_textptr, item_cchText, item_image, item_selimage, \ item_cChildren, item_param = struct.unpack("10i", buffer) # ensure only items listed by the mask are valid (except we assume the # handle is always valid - some notifications (eg, TVN_ENDLABELEDIT) set a # mask that doesn't include the handle, but the docs explicity say it is.) if not (item_mask & commctrl.TVIF_TEXT): item_textptr = item_cchText = None if not (item_mask & commctrl.TVIF_CHILDREN): item_cChildren = None if not (item_mask & commctrl.TVIF_IMAGE): item_image = None if not (item_mask & commctrl.TVIF_PARAM): item_param = None if not (item_mask & commctrl.TVIF_SELECTEDIMAGE): item_selimage = None if not (item_mask & commctrl.TVIF_STATE): item_state = item_stateMask = None if item_textptr: text = win32gui.PyGetString(item_textptr) else: text = None return item_hItem, item_state, item_stateMask, \ text, item_image, item_selimage, \ item_cChildren, item_param def UnpackTVNOTIFY(lparam): format = "iiii40s40s" buf = win32gui.PyMakeBuffer(struct.calcsize(format), lparam) hwndFrom, id, code, action, buf_old, buf_new \ = struct.unpack(format, buf) item_old = UnpackTVItem(buf_old) item_new = UnpackTVItem(buf_new) return hwndFrom, id, code, action, item_old, item_new def UnpackTVDISPINFO(lparam): format = "iii40s" buf = win32gui.PyMakeBuffer(struct.calcsize(format), lparam) hwndFrom, id, code, buf_item = struct.unpack(format, buf) item = UnpackTVItem(buf_item) return hwndFrom, id, code, item # XXX - end of code copied to win32gui_struct.py ######################################################################### ## The dialog itself ######################################################################### import dlgcore FolderSelector_Parent = dlgcore.TooltipDialog class FolderSelector(FolderSelector_Parent): def __init__ (self, parent, manager, selected_ids=None, single_select=False, checkbox_state=False, checkbox_text=None, desc_noun="Select", desc_noun_suffix="ed", exclude_prop_ids=(PR_IPM_WASTEBASKET_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_IPM_OUTBOX_ENTRYID) ): FolderSelector_Parent.__init__(self, parent, manager.dialog_parser, "IDD_FOLDER_SELECTOR") assert not single_select or selected_ids is None or len(selected_ids)<=1 self.single_select = single_select self.next_item_id = 1 self.item_map = {} self.timer_id = None self.imageList = None self.select_desc_noun = desc_noun self.select_desc_noun_suffix = desc_noun_suffix self.selected_ids = [sid for sid in selected_ids if sid is not None] self.manager = manager self.checkbox_state = checkbox_state self.checkbox_text = checkbox_text or "Include &subfolders" self.exclude_prop_ids = exclude_prop_ids self.in_label_edit = False self.in_check_selections_valid = False def CompareIDs(self, id1, id2): # Compare the eid of the stores, then the objects CompareEntryIDs = self.manager.message_store.session.CompareEntryIDs try: return CompareEntryIDs(mapi.BinFromHex(id1[0]), mapi.BinFromHex(id2[0])) and \ CompareEntryIDs(mapi.BinFromHex(id1[1]), mapi.BinFromHex(id2[1])) except pythoncom.com_error: # invalid IDs are never the same return False def InIDs(self, id, ids): for id_check in ids: if self.CompareIDs(id_check, id): return True return False def _MakeItemParam(self, item): item_id = self.next_item_id self.next_item_id += 1 self.item_map[item_id] = item return item_id def _InsertFolder(self, hParent, child, selected_ids = None, insert_after=0): text = child.name if child.children is None: # Need to build them! cItems = 1 # Anything > 0 will do else: cItems = len(child.children) if cItems==0: bitmapCol = bitmapSel = 5 # blank doc else: bitmapCol = bitmapSel = 0 # folder if self.single_select: mask = state = 0 else: if (selected_ids and self.InIDs(child.folder_id, selected_ids)): state = INDEXTOSTATEIMAGEMASK(IIL_CHECKED) else: state = INDEXTOSTATEIMAGEMASK(IIL_UNCHECKED) mask = commctrl.TVIS_STATEIMAGEMASK item_id = self._MakeItemParam(child) insert_buf, extras = PackTVINSERTSTRUCT(hParent, insert_after, (None, state, mask, text, bitmapCol, bitmapSel, cItems, item_id)) if verbose: print "Inserting item", repr(insert_buf), "-", hitem = win32gui.SendMessage(self.list, commctrl.TVM_INSERTITEM, 0, insert_buf) if verbose: print "got back handle", hitem return hitem def _InsertSubFolders(self, hParent, folderSpec): for child in folderSpec.children: hitem = self._InsertFolder(hParent, child, self.selected_ids) # If this folder is in the list of ones we need to expand # to show pre-selected items, then force expand now. if self.InIDs(child.folder_id, self.expand_ids): win32gui.SendMessage(self.list, commctrl.TVM_EXPAND, commctrl.TVE_EXPAND, hitem) # If single-select, and this is ours, select it # (multi-select uses check-boxes, not selection) if (self.single_select and self.selected_ids and self.InIDs(child.folder_id, self.selected_ids)): win32gui.SendMessage(self.list, commctrl.TVM_SELECTITEM, commctrl.TVGN_CARET, hitem) def _DetermineFoldersToExpand(self): folders_to_expand = [] for folder_id in self.selected_ids: try: folder = self.manager.message_store.GetFolder(folder_id) except self.manager.message_store.MsgStoreException, details: print "Can't find a folder to expand:", details folder = None while folder is not None: try: parent = folder.GetParent() except self.manager.message_store.MsgStoreException, details: print "Can't find folder's parent:", details parent = None if parent is not None and \ not self.InIDs(parent.GetID(), folders_to_expand): folders_to_expand.append(parent.GetID()) folder = parent return folders_to_expand def _GetTVItem(self, h): buffer, extra = EmptyTVITEM(h) win32gui.SendMessage(self.list, commctrl.TVM_GETITEM, 0, buffer.buffer_info()[0]) return UnpackTVItem(buffer.tostring()) def _YieldChildren(self, h): try: h = win32gui.SendMessage(self.list, commctrl.TVM_GETNEXTITEM, commctrl.TVGN_CHILD, h) except win32gui.error: h = 0 while h: info = self._GetTVItem(h) item_param = info[-1] spec = self.item_map[item_param] yield info, spec # Check children for info, spec in self._YieldChildren(h): yield info, spec try: h = win32gui.SendMessage(self.list, commctrl.TVM_GETNEXTITEM, commctrl.TVGN_NEXT, h) except win32gui.error: h = None def _YieldAllChildren(self): return self._YieldChildren(commctrl.TVI_ROOT) def _YieldCheckedChildren(self): if self.single_select: # If single-select, the checked state is not used, just the # selected state. try: h = win32gui.SendMessage(self.list, commctrl.TVM_GETNEXTITEM, commctrl.TVGN_CARET, 0) except win32gui.error: h = 0 if not h: # nothing selected. return info = self._GetTVItem(h) spec = self.item_map[info[7]] yield info, spec return # single-hit yield. for info, spec in self._YieldAllChildren(): checked = (info[1] >> 12) - 1 if checked: yield info, spec def GetSelectedIDs(self): try: self.GetDlgItem("IDC_LIST_FOLDERS") except win32gui.error: # dialog dead! return self.selected_ids, self.checkbox_state ret = [] for info, spec in self._YieldCheckedChildren(): ret.append(spec.folder_id) check = win32gui.SendMessage(self.GetDlgItem("IDC_BUT_SEARCHSUB"), win32con.BM_GETCHECK, 0, 0) return ret, check != 0 def UnselectItem(self, item): if self.single_select: win32gui.SendMessage(self.list, commctrl.TVM_SELECTITEM, commctrl.TVGN_CARET, 0) else: state = INDEXTOSTATEIMAGEMASK(IIL_UNCHECKED) mask = commctrl.TVIS_STATEIMAGEMASK buf, extra = PackTVITEM(item[0], state, mask, None, None, None, None, None) win32gui.SendMessage(self.list, commctrl.TVM_SETITEM, 0, buf) def _CheckSelectionsValid(self, is_close = False): if self.in_check_selections_valid: return self.in_check_selections_valid = True try: if self.single_select: if is_close: # Make sure one is selected. for ignore in self._YieldCheckedChildren(): break else: self.manager.ReportInformation("You must select a folder") return False else: # In a single-select dialog, we can't stop the user selecting # a 'top-level' folder - we can only stop them closing the # dialog while it is selected. return True # For a multi-select dialog, we simply un-check the existing item. # For single-select, we set no item selected. result_valid = True for info, spec in self._YieldCheckedChildren(): try: folder = self.manager.message_store.GetFolder(spec.folder_id) parent = folder.GetParent() try: # Psts and the main Exchange store have top level # folders with parents (with empty display names), # and no grandparents. Anything below the top # level *does* have a grandparent. This means our # test for "top level folder" can be: does it have # a parent *and* grandparent. However, a # secondary Exchange account doesn't have the # empty-display-name parent, so the top-level # doesn't have a parent, and the top selectable # folder doesn't have a grandparent, and our test # fails. Allow for this by checking for the # "Access denied" exception when getting the # grandparent, and assuming that this means that # this is what is happening. This will only fail # if we get an 'access denied' error for the # empty-display-name parent, which should not be # the case. grandparent = parent.GetParent() except self.manager.message_store.MsgStoreException, details: hr, msg, exc, argErr = details.mapi_exception if hr == winerror.E_ACCESSDENIED: valid = parent is not None else: raise # but only down a couple of lines... else: valid = parent is not None and grandparent is not None except self.manager.message_store.MsgStoreException, details: print "Eeek - couldn't get the folder to check " \ "valid:", details valid = False if not valid: if result_valid: # are we the first invalid? self.manager.ReportInformation( "Please select a child folder - top-level folders " \ "can not be used.") self.UnselectItem(info) result_valid = result_valid and valid return result_valid finally: self.in_check_selections_valid = False # Message processing # def GetMessageMap(self): def OnInitDialog (self, hwnd, msg, wparam, lparam): FolderSelector_Parent.OnInitDialog(self, hwnd, msg, wparam, lparam) caption = "%s folder" % (self.select_desc_noun,) if not self.single_select: caption += "(s)" win32gui.SendMessage(hwnd, win32con.WM_SETTEXT, 0, caption) self.SetDlgItemText("IDC_BUT_SEARCHSUB", self.checkbox_text) child = self.GetDlgItem("IDC_BUT_SEARCHSUB") if self.checkbox_state is None: win32gui.ShowWindow(child, win32con.SW_HIDE) else: win32gui.SendMessage(child, win32con.BM_SETCHECK, self.checkbox_state) self.list = self.GetDlgItem("IDC_LIST_FOLDERS") import resources mod_handle, mod_bmp, extra_flags = \ resources.GetImageParamsFromBitmapID(self.dialog_parser, "IDB_FOLDERS") bitmapMask = win32api.RGB(0,0,255) self.imageList = win32gui.ImageList_LoadImage(mod_handle, mod_bmp, 16, 0, bitmapMask, win32con.IMAGE_BITMAP, extra_flags) win32gui.SendMessage( self.list, commctrl.TVM_SETIMAGELIST, commctrl.TVSIL_NORMAL, self.imageList ) if self.single_select: # Remove the checkbox style from the list for single-selection style = win32api.GetWindowLong(self.list, win32con.GWL_STYLE) style = style & ~commctrl.TVS_CHECKBOXES win32api.SetWindowLong(self.list, win32con.GWL_STYLE, style) # Hide "clear all" child = self.GetDlgItem("IDC_BUT_CLEARALL") win32gui.ShowWindow(child, win32con.SW_HIDE) # Extended MAPI version of the tree. # Build list of all ids to expand - ie, list includes all # selected folders, and all parents. dlgutils.SetWaitCursor(1) self.expand_ids = self._DetermineFoldersToExpand() tree = BuildFolderTreeMAPI(self.manager.message_store.session, self.exclude_prop_ids) self._InsertSubFolders(0, tree) self.selected_ids = [] # Only use this while creating dialog. self.expand_ids = [] # Only use this while creating dialog. self._UpdateStatus() dlgutils.SetWaitCursor(0) def OnDestroy(self, hwnd, msg, wparam, lparam): import timer if self.timer_id is not None: timer.kill_timer(self.timer_id) self.item_map = None if self.imageList: win32gui.ImageList_Destroy(self.imageList) FolderSelector_Parent.OnDestroy(self, hwnd, msg, wparam, lparam) def OnCommand(self, hwnd, msg, wparam, lparam): FolderSelector_Parent.OnCommand(self, hwnd, msg, wparam, lparam) id = win32api.LOWORD(wparam) id_name = self._GetIDName(id) code = win32api.HIWORD(wparam) if code == win32con.BN_CLICKED: if id in (win32con.IDOK, win32con.IDCANCEL) and self.in_label_edit: cancel = id == win32con.IDCANCEL win32gui.SendMessage(self.list, commctrl.TVM_ENDEDITLABELNOW, cancel,0) return # Button clicks if id == win32con.IDOK: if not self._CheckSelectionsValid(True): return self.selected_ids, self.checkbox_state = self.GetSelectedIDs() win32gui.EndDialog(hwnd, id) elif id == win32con.IDCANCEL: win32gui.EndDialog(hwnd, id) elif id_name == "IDC_BUT_CLEARALL": for info, spec in self._YieldCheckedChildren(): self.UnselectItem(info) elif id_name == "IDC_BUT_NEW": # Force a new entry in the tree at our location, and begin # editing. # Add the new item to the tree. h = win32gui.SendMessage(self.list, commctrl.TVM_GETNEXTITEM, commctrl.TVGN_CARET, commctrl.TVI_ROOT) parent_item = self._GetTVItem(h) if parent_item[6]==0: # eeek - parent has no existig children - say we have one # so we can be expanded. update_item, extra = PackTVITEM(h, None, None, None, None, None, 1, None) win32gui.SendMessage(self.list, commctrl.TVM_SETITEM, 0, update_item) item_id = self._MakeItemParam(None) temp_spec = FolderSpec(None, "New folder") hnew = self._InsertFolder(h, temp_spec, None, commctrl.TVI_FIRST) win32gui.SendMessage(self.list, commctrl.TVM_ENSUREVISIBLE, 0, hnew) win32gui.SendMessage(self.list, commctrl.TVM_SELECTITEM, commctrl.TVGN_CARET, hnew) # Allow label editing s = win32api.GetWindowLong(self.list, win32con.GWL_STYLE) s |= commctrl.TVS_EDITLABELS win32api.SetWindowLong(self.list, win32con.GWL_STYLE, s) win32gui.SetFocus(self.list) self.in_label_edit = True win32gui.SendMessage(self.list, commctrl.TVM_EDITLABEL, 0, hnew) self._UpdateStatus() def _DoUpdateStatus(self, id, timeval): import timer # Kill the timer first to prevent it firing again. self.timer_id = None timer.kill_timer(id) self._CheckSelectionsValid() names = [] num_checked = 0 for info, spec in self._YieldCheckedChildren(): num_checked += 1 if len(names) < 20: names.append(info[3]) status_string = "%s%s %d folder" % (self.select_desc_noun, self.select_desc_noun_suffix, num_checked) if num_checked != 1: status_string += "s" self.SetDlgItemText("IDC_STATUS1", status_string) self.SetDlgItemText("IDC_STATUS2", "; ".join(names)) def _UpdateStatus(self): # We have problems with the order of events - we get the notification # events before the new states are available via GetItem. # Therefore, we start a one-shot, immediate timer, which ends up # at the end of the message queue, and we work. import timer if self.timer_id is not None: timer.kill_timer(self.timer_id) self.timer_id = timer.set_timer (0, self._DoUpdateStatus) def OnNotify(self, msg, hwnd, wparam, lparam): FolderSelector_Parent.OnNotify(self, hwnd, msg, wparam, lparam) format = "iii" buf = win32gui.PyMakeBuffer(struct.calcsize(format), lparam) hwndFrom, id, code = struct.unpack(format, buf) code += commctrl.PY_0U # work around silly old pywin32 bug id_name = self._GetIDName(id) if id_name == "IDC_LIST_FOLDERS": if code == commctrl.NM_CLICK: self._UpdateStatus() elif code == commctrl.NM_DBLCLK: # No special dblclick handling - default behaviour is to # expand/collapse tree, and auto-closing the dialog, even # when the folder has no children, doesn't really make sense. pass elif code == commctrl.TVN_ITEMEXPANDING: ignore, ignore, ignore, action, itemOld, itemNew = \ UnpackTVNOTIFY(lparam) if action == 1: return 0 # contracting, not expanding itemHandle = itemNew[0] info = itemNew folderSpec = self.item_map[info[7]] if folderSpec.children is None: folderSpec.children = _BuildFoldersMAPI(self.manager, folderSpec) self._InsertSubFolders(itemHandle, folderSpec) elif code == commctrl.TVN_SELCHANGED: self._UpdateStatus() elif code == commctrl.TVN_ENDLABELEDIT: ignore, ignore, ignore, item = UnpackTVDISPINFO(lparam) handle = item[0] stay_in_edit = False try: name = item[3] if name is None: # User cancelled folder creation - delete the item win32gui.SendMessage(self.list, commctrl.TVM_DELETEITEM, 0, handle) return # Attempt to create a folder of that name. parent_handle = win32gui.SendMessage(self.list, commctrl.TVM_GETNEXTITEM, commctrl.TVGN_PARENT, handle) parent_item = self._GetTVItem(parent_handle) parent_spec = self.item_map[parent_item[7]] parent_folder = self.manager.message_store.GetFolder(parent_spec.folder_id) try: new_folder = parent_folder.CreateFolder(name) # Create a new FolderSpec for this folder, and stash new_spec = FolderSpec(new_folder.GetID(), name) # The info passed by the notify message appears to # not have the lparam (even though the docs say it # does.) Fetch it spec_key = self._GetTVItem(handle)[7] self.item_map[spec_key] = new_spec # And update the tree with the new item buf, extra = PackTVITEM(handle, None, None, name, None, None, None, None) win32gui.SendMessage(self.list, commctrl.TVM_SETITEM, 0, buf) except pythoncom.com_error, details: hr, msg, exc, arg = details if hr == mapi.MAPI_E_COLLISION: user_msg = "A folder with that name already exists" else: user_msg = "MAPI error %s" % mapiutil.GetScodeString(hr) self.manager.ReportError("Could not create the folder\r\n\r\n" + user_msg) stay_in_edit = True finally: if stay_in_edit: win32gui.SendMessage(self.list, commctrl.TVM_EDITLABEL, 0, handle) else: # reset to no label edits s = win32api.GetWindowLong(self.list, win32con.GWL_STYLE) s &= ~commctrl.TVS_EDITLABELS win32api.SetWindowLong(self.list, win32con.GWL_STYLE, s) self.in_label_edit = False def Test(): single_select =False import sys, os sys.path.append(os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), ".."))) import manager mgr = manager.GetManager() if mgr.dialog_parser is None: import dialogs mgr.dialog_parser = dialogs.LoadDialogs() ids = [("0000","0000"),] # invalid ID for testing. d=FolderSelector(0, mgr, ids, single_select = single_select) if d.DoModal() != win32con.IDOK: print "Cancelled" return ids, include_sub = d.GetSelectedIDs() d=FolderSelector(0, mgr, ids, single_select = single_select, checkbox_state = include_sub) d.DoModal() if __name__=='__main__': verbose = 1 Test() spambayes-1.1a6/Outlook2000/dialogs/opt_processors.py0000664000076500000240000003616310646440134022712 0ustar skipstaff00000000000000# Option Control Processors for our dialog. # These are extensions to basic Control Processors that are linked with # SpamBayes options. # This module is part of the spambayes project, which is Copyright 2003 # The Python Software Foundation and is covered by the Python Software # Foundation license. import win32gui, win32api, win32con import commctrl import struct, array from dlgutils import * import processors verbose = 0 # set to 1 to see option values fetched and set. # A ControlProcessor that is linked up with options. These get a bit smarter. class OptionControlProcessor(processors.ControlProcessor): def __init__(self, window, control_ids, option): processors.ControlProcessor.__init__(self, window, control_ids) if option: sect_name, option_name = option.split(".") self.option = window.config.get_option(sect_name, option_name) else: self.option = None def GetPopupHelpText(self, idFrom): doc = " ".join(self.option.doc().split()) if self.option.default_value: doc += " (the default value is %s)" % self.option.default_value return doc # We override Init, and break it into 2 steps. def Init(self): self.UpdateControl_FromValue() def Done(self): self.UpdateValue_FromControl() return True def NotifyOptionChanged(self, option = None): if option is None: option = self.option self.window.OnOptionChanged(self, option) def SetOptionValue(self, value, option = None): if option is None: option = self.option if verbose: print "Setting option '%s' (%s) -> %s" % \ (option.display_name(), option.name, value) option.set(value) self.NotifyOptionChanged(option) def GetOptionValue(self, option = None): if option is None: option = self.option ret = option.get() if verbose: print "Got option '%s' (%s) -> %s" % \ (option.display_name(), option.name, ret) return ret # Only sub-classes know how to update their controls from the value. def UpdateControl_FromValue(self): raise NotImplementedError def UpdateValue_FromControl(self): raise NotImplementedError # "Bool" buttons are simple - just toggle the value on the click. # (Little more complex to handle "radio buttons" that are also boolean # where we must "uncheck" the other button. class BoolButtonProcessor(OptionControlProcessor): def __init__(self, window, control_ids, option, disable_when_false_ids=""): OptionControlProcessor.__init__(self, window, control_ids, option) self.disable_ids = [window.manager.dialog_parser.ids[id] for id in disable_when_false_ids.split()] def OnCommand(self, wparam, lparam): code = win32api.HIWORD(wparam) if code == win32con.BN_CLICKED: self.UpdateValue_FromControl() def UpdateEnabledStates(self, enabled): for other in self.disable_ids: win32gui.EnableWindow(self.GetControl(other), enabled) def UpdateControl_FromValue(self): value = self.GetOptionValue() win32gui.SendMessage(self.GetControl(), win32con.BM_SETCHECK, value) for other in self.other_ids: win32gui.SendMessage(self.GetControl(other), win32con.BM_SETCHECK, not value) self.UpdateEnabledStates(value) def UpdateValue_FromControl(self): check = win32gui.SendMessage(self.GetControl(), win32con.BM_GETCHECK) check = not not check # force bool! self.SetOptionValue(check) self.UpdateEnabledStates(check) class RadioButtonProcessor(OptionControlProcessor): def OnCommand(self, wparam, lparam): code = win32api.HIWORD(wparam) if code == win32con.BN_CLICKED: self.UpdateValue_FromControl() def UpdateControl_FromValue(self): value = self.GetOptionValue() i = 0 first = chwnd = self.GetControl() while chwnd: if i==value: win32gui.SendMessage(chwnd, win32con.BM_SETCHECK, 1) break chwnd = win32gui.GetNextDlgGroupItem(self.window.hwnd, chwnd, False) assert chwnd!=first, "Back where I started!" i += 1 else: assert 0, "Could not find control for value %s" % value def UpdateValue_FromControl(self): all_ids = [self.control_id] + self.other_ids chwnd = self.GetControl() i = 0 while chwnd: checked = win32gui.SendMessage(chwnd, win32con.BM_GETCHECK) if checked: self.SetOptionValue(i) break chwnd = win32gui.GetNextDlgGroupItem(self.window.hwnd, chwnd, False) i += 1 else: assert 0, "Couldn't find a checked button" # A "Combo" processor, that loads valid strings from the option. class ComboProcessor(OptionControlProcessor): def __init__(self, window, control_ids, option,text=None): OptionControlProcessor.__init__(self, window, control_ids, option) if text: temp = text.split(",") self.option_to_text = zip(self.option.valid_input(), temp) self.text_to_option = dict(zip(temp, self.option.valid_input())) else: self.option_to_text = zip(self.option.valid_input(),self.option.valid_input()) self.text_to_option = dict(self.option_to_text) def OnCommand(self, wparam, lparam): code = win32api.HIWORD(wparam) if code == win32con.CBN_SELCHANGE: self.UpdateValue_FromControl() def UpdateControl_FromValue(self): # First load the combo options. combo = self.GetControl() index = sel_index = 0 value = self.GetOptionValue() for opt,text in self.option_to_text: win32gui.SendMessage(combo, win32con.CB_ADDSTRING, 0, text) if value.startswith(opt): sel_index = index index += 1 win32gui.SendMessage(combo, win32con.CB_SETCURSEL, sel_index, 0) def UpdateValue_FromControl(self): combo = self.GetControl() sel = win32gui.SendMessage(combo, win32con.CB_GETCURSEL) len = win32gui.SendMessage(combo, win32con.CB_GETLBTEXTLEN, sel) buffer = array.array("c", "\0" * (len + 1)) win32gui.SendMessage(combo, win32con.CB_GETLBTEXT, sel, buffer) # Trim the \0 from the end. text = buffer.tostring()[:-1] self.SetOptionValue(self.text_to_option[text]) class EditNumberProcessor(OptionControlProcessor): def __init__(self, window, control_ids, option, min_val=0, max_val=100, ticks=100, max_edit_val=100): self.slider_id = control_ids and control_ids[1] self.min_val = min_val self.max_val = max_val self.max_edit_val = max_edit_val self.ticks = ticks OptionControlProcessor.__init__(self, window, control_ids, option) def GetPopupHelpText(self, id): if id == self.slider_id: return "As you drag this slider, the value to the right will " \ "automatically adjust" return OptionControlProcessor.GetPopupHelpText(self, id) def GetMessages(self): return [win32con.WM_HSCROLL] def OnMessage(self, msg, wparam, lparam): slider = self.GetControl(self.slider_id) if slider == lparam: slider_pos = win32gui.SendMessage(slider, commctrl.TBM_GETPOS, 0, 0) slider_pos = float(slider_pos) * self.max_val / self.ticks str_val = str(slider_pos) edit = self.GetControl() win32gui.SendMessage(edit, win32con.WM_SETTEXT, 0, str_val) def OnCommand(self, wparam, lparam): code = win32api.HIWORD(wparam) if code==win32con.EN_CHANGE: try: self.UpdateValue_FromControl() self.UpdateSlider_FromEdit() except ValueError: # They are typing - value may be currently invalid pass def Init(self): OptionControlProcessor.Init(self) if self.slider_id: self.InitSlider() def InitSlider(self): slider = self.GetControl(self.slider_id) # xxx - this wont be right if min <> 0 :( assert self.min_val == 0, "sue me" win32gui.SendMessage(slider, commctrl.TBM_SETRANGE, 0, MAKELONG(0, self.ticks)) # sigh - these values may not be right win32gui.SendMessage(slider, commctrl.TBM_SETLINESIZE, 0, 1) win32gui.SendMessage(slider, commctrl.TBM_SETPAGESIZE, 0, self.ticks/20) win32gui.SendMessage(slider, commctrl.TBM_SETTICFREQ, self.ticks/10, 0) def UpdateControl_FromValue(self): win32gui.SendMessage(self.GetControl(), win32con.WM_SETTEXT, 0, str(self.GetOptionValue())) self.UpdateSlider_FromEdit() def UpdateSlider_FromEdit(self): slider = self.GetControl(self.slider_id) # done as the user is typing into the edit control, so we must not # complain here about invalid values as it is likely to only be # temporarily invalid until they finish. try: # Get as float so we dont fail should the .0 be there, but # then convert to int as the slider only works with ints val = float(self.GetOptionValue()) # Convert it to our range. val *= float(self.ticks) / self.max_val val = int(val) except ValueError: return win32gui.SendMessage(slider, commctrl.TBM_SETPOS, 1, val) def UpdateValue_FromControl(self): buf_size = 100 buf = win32gui.PyMakeBuffer(buf_size) nchars = win32gui.SendMessage(self.GetControl(), win32con.WM_GETTEXT, buf_size, buf) str_val = buf[:nchars] val = float(str_val) if val < self.min_val or val > self.max_edit_val: raise ValueError, "Value must be between %d and %d" % (self.min_val, self.max_val) self.SetOptionValue(val) class FilenameProcessor(OptionControlProcessor): def __init__(self, window, control_ids, option, file_filter="All Files|*.*"): self.button_id = control_ids[1] self.file_filter = file_filter OptionControlProcessor.__init__(self, window, control_ids, option) def GetPopupHelpText(self, idFrom): if idFrom == self.button_id: return "Displays a dialog from which you can select a file." return OptionControlProcessor.GetPopupHelpText(self, id) def DoBrowse(self): from win32struct import OPENFILENAME ofn = OPENFILENAME(512) ofn.hwndOwner = self.window.hwnd ofn.setFilter(self.file_filter) ofn.setTitle(_("Browse for file")) def_filename = self.GetOptionValue() if (len(def_filename) > 0): from os.path import basename ofn.setInitialDir(basename(def_filename)) ofn.setFilename(def_filename) ofn.Flags = win32con.OFN_FILEMUSTEXIST retval = win32gui.GetOpenFileName(str(ofn)) if (retval == win32con.IDOK): self.SetOptionValue(ofn.getFilename()) self.UpdateControl_FromValue() return True return False def OnCommand(self, wparam, lparam): id = win32api.LOWORD(wparam) code = win32api.HIWORD(wparam) if id == self.button_id: self.DoBrowse() elif code==win32con.EN_CHANGE: self.UpdateValue_FromControl() def UpdateControl_FromValue(self): win32gui.SendMessage(self.GetControl(), win32con.WM_SETTEXT, 0, self.GetOptionValue()) def UpdateValue_FromControl(self): buf_size = 256 buf = win32gui.PyMakeBuffer(buf_size) nchars = win32gui.SendMessage(self.GetControl(), win32con.WM_GETTEXT, buf_size, buf) str_val = buf[:nchars] self.SetOptionValue(str_val) # Folder IDs, and the "include_sub" option, if applicable. class FolderIDProcessor(OptionControlProcessor): def __init__(self, window, control_ids, option, option_include_sub = None, use_fqn = False, name_joiner = "; "): self.button_id = control_ids[1] self.use_fqn = use_fqn self.name_joiner = name_joiner if option_include_sub: incl_sub_sect_name, incl_sub_option_name = \ option_include_sub.split(".") self.option_include_sub = \ window.config.get_option(incl_sub_sect_name, incl_sub_option_name) else: self.option_include_sub = None OptionControlProcessor.__init__(self, window, control_ids, option) def DoBrowse(self): mgr = self.window.manager is_multi = self.option.multiple_values_allowed() if is_multi: ids = self.GetOptionValue() else: ids = [self.GetOptionValue()] from dialogs import FolderSelector if self.option_include_sub: cb_state = self.option_include_sub.get() else: cb_state = None # don't show it. d = FolderSelector.FolderSelector(self.window.hwnd, mgr, ids, single_select=not is_multi, checkbox_state=cb_state) if d.DoModal() == win32con.IDOK: ids, include_sub = d.GetSelectedIDs() if is_multi: self.SetOptionValue(ids) else: self.SetOptionValue(ids[0]) if self.option_include_sub: self.SetOptionValue(include_sub, self.option_include_sub) self.UpdateControl_FromValue() return True return False def OnCommand(self, wparam, lparam): id = win32api.LOWORD(wparam) if id == self.button_id: self.DoBrowse() def GetPopupHelpText(self, idFrom): if idFrom == self.button_id: return "Displays a list from which you can select folders." return OptionControlProcessor.GetPopupHelpText(self, idFrom) def UpdateControl_FromValue(self): # Set the static to folder names mgr = self.window.manager if self.option.multiple_values_allowed(): ids = self.GetOptionValue() else: ids = [self.GetOptionValue()] names = [] for eid in ids: if eid is not None: try: folder = mgr.message_store.GetFolder(eid) if self.use_fqn: name = folder.GetFQName() else: name = folder.name except mgr.message_store.MsgStoreException: name = "" names.append(name) win32gui.SetWindowText(self.GetControl(), self.name_joiner.join(names)) def UpdateValue_FromControl(self): pass spambayes-1.1a6/Outlook2000/dialogs/processors.py0000664000076500000240000000754010646440134022025 0ustar skipstaff00000000000000# Control Processors for our dialog. # This module is part of the spambayes project, which is Copyright 2003 # The Python Software Foundation and is covered by the Python Software # Foundation license. import win32gui, win32api, win32con import commctrl import struct, array from dlgutils import * # Cache our leaky bitmap handles bitmap_handles = {} # A generic set of "ControlProcessors". A control processor by itself only # does a few basic things. class ControlProcessor: def __init__(self, window, control_ids): self.control_id = control_ids[0] self.other_ids = control_ids[1:] self.window = window def Init(self): pass def Done(self): # done with 'ok' - ie, save options. May return false. return True def Term(self): # closing - can't fail. pass def GetControl(self, control_id = None): control_id = control_id or self.control_id try: h = win32gui.GetDlgItem(self.window.hwnd, control_id) except: hparent = win32gui.GetParent(self.window.hwnd) hparent = win32gui.GetParent(hparent) h = win32gui.GetDlgItem(hparent, control_id) return h def GetPopupHelpText(self, idFrom): return None def OnCommand(self, wparam, lparam): pass def OnNotify(self, nmhdr, wparam, lparam): pass def GetMessages(self): return [] def OnMessage(self, msg, wparam, lparam): raise RuntimeError, "I don't hook any messages, so I shouldn't be called" def OnOptionChanged(self, option): pass def OnRButtonUp(self, wparam, lparam): pass class ImageProcessor(ControlProcessor): def Init(self): rcp = self.window.manager.dialog_parser; bmp_id = int(win32gui.GetWindowText(self.GetControl())) if bitmap_handles.has_key(bmp_id): handle = bitmap_handles[bmp_id] else: import resources mod_handle, mod_bmp, extra_flags = resources.GetImageParamsFromBitmapID(rcp, bmp_id) load_flags = extra_flags|win32con.LR_COLOR|win32con.LR_SHARED handle = win32gui.LoadImage(mod_handle, mod_bmp, win32con.IMAGE_BITMAP,0,0,load_flags) bitmap_handles[bmp_id] = handle win32gui.SendMessage(self.GetControl(), win32con.STM_SETIMAGE, win32con.IMAGE_BITMAP, handle) def GetPopupHelpText(self, cid): return None class ButtonProcessor(ControlProcessor): def OnCommand(self, wparam, lparam): code = win32api.HIWORD(wparam) id = win32api.LOWORD(wparam) if code == win32con.BN_CLICKED: self.OnClicked(id) class CloseButtonProcessor(ButtonProcessor): def OnClicked(self, id): problem = self.window.manager.GetDisabledReason() if problem: q = _("There appears to be a problem with SpamBayes' configuration" \ "\r\nIf you do not fix this problem, SpamBayes will be" \ " disabled.\r\n\r\n%s" \ "\r\n\r\nDo you wish to re-configure?") % (problem,) if self.window.manager.AskQuestion(q): return win32gui.EndDialog(self.window.hwnd, id) def GetPopupHelpText(self, ctrlid): return _("Closes this dialog") class CommandButtonProcessor(ButtonProcessor): def __init__(self, window, control_ids, func, args): assert len(control_ids)==1 self.func = func self.args = args ControlProcessor.__init__(self, window, control_ids) def OnClicked(self, id): # Bit of a hack - always pass the manager as the first arg. args = (self.window,) + self.args self.func(*args) def GetPopupHelpText(self, ctrlid): assert ctrlid == self.control_id doc = self.func.__doc__ if doc is None: return "" return " ".join(doc.split()) spambayes-1.1a6/Outlook2000/dialogs/resources/0000775000076500000240000000000011355064626021263 5ustar skipstaff00000000000000spambayes-1.1a6/Outlook2000/dialogs/resources/__init__.py0000664000076500000240000000150510646440134023367 0ustar skipstaff00000000000000# Package that manages and defines dialog resources def GetImageParamsFromBitmapID(rc_parser, bmpid): import os, sys import win32gui, win32con, win32api if type(bmpid)==type(0): bmpid = rc_parser.names[bmpid] int_bmpid = rc_parser.ids[bmpid] filename = rc_parser.bitmaps[bmpid] if hasattr(sys, "frozen"): # in our .exe/.dll - load from that. if sys.frozen=="dll": hmod = sys.frozendllhandle else: hmod = win32api.GetModuleHandle(None) return hmod, int_bmpid, 0 else: # source code - load the .bmp directly. if not os.path.isabs(filename): # In this directory filename = os.path.join( os.path.dirname( __file__ ), filename) return 0, filename, win32con.LR_LOADFROMFILE assert 0, "not reached" spambayes-1.1a6/Outlook2000/dialogs/resources/dialogs.h0000664000076500000240000001230610646440134023052 0ustar skipstaff00000000000000//{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by dialogs.rc // #define IDD_MANAGER 101 #define IDD_TRAINING 102 #define IDD_FILTER 103 #define IDD_FILTER_NOW 104 #define IDD_FOLDER_SELECTOR 105 #define IDD_ADVANCED 106 #define IDD_STATISTICS 107 #define IDD_GENERAL 108 #define IDD_FILTER_SPAM 110 #define IDD_DIAGNOSTIC 113 #define IDD_WIZARD 114 #define IDD_WIZARD_WELCOME 115 #define IDD_WIZARD_FINISHED_UNTRAINED 116 #define IDD_WIZARD_FOLDERS_REST 117 #define IDD_WIZARD_FOLDERS_WATCH 118 #define IDD_WIZARD_FINISHED_UNCONFIGURED 119 #define IDD_WIZARD_FOLDERS_TRAIN 120 #define IDD_WIZARD_TRAIN 121 #define IDD_WIZARD_FINISHED_TRAINED 122 #define IDD_WIZARD_TRAINING_IS_IMPORTANT 123 #define IDD_WIZARD_FINISHED_TRAIN_LATER 124 #define IDB_SBWIZLOGO 125 #define IDB_FOLDERS 127 #define IDD_NOTIFICATIONS 128 #define IDC_PROGRESS 1000 #define IDC_PROGRESS_TEXT 1001 #define IDC_STATIC_HAM 1002 #define IDC_STATIC_SPAM 1003 #define IDC_BROWSE_HAM 1004 #define IDC_BROWSE_SPAM 1005 #define IDC_START 1006 #define IDC_BUT_REBUILD 1007 #define IDC_BUT_RESCORE 1008 #define IDC_VERSION 1009 #define IDC_BUT_TRAIN_FROM_SPAM_FOLDER 1010 #define IDC_BUT_TRAIN_TO_SPAM_FOLDER 1011 #define IDC_BUT_TRAIN_NOW 1012 #define IDC_BUT_FILTER_ENABLE 1013 #define IDC_FILTER_STATUS 1014 #define IDC_BUT_FILTER_DEFINE 1016 #define IDC_BUT_ABOUT 1017 #define IDC_BUT_ACT_SCORE 1018 #define IDC_BUT_ACT_ALL 1019 #define IDC_BUT_UNREAD 1020 #define IDC_BUT_UNSEEN 1021 #define IDC_SLIDER_CERTAIN 1023 #define IDC_EDIT_CERTAIN 1024 #define IDC_ACTION_CERTAIN 1025 #define IDC_FOLDER_CERTAIN 1027 #define IDC_BROWSE_CERTAIN 1028 #define IDC_SLIDER_UNSURE 1029 #define IDC_EDIT_UNSURE 1030 #define IDC_ACTION_UNSURE 1031 #define IDC_ACTION_HAM 1032 #define IDC_FOLDER_UNSURE 1033 #define IDC_BROWSE_UNSURE 1034 #define IDC_TRAINING_STATUS 1035 #define IDC_FOLDER_NAMES 1036 #define IDC_BROWSE 1037 #define IDC_FOLDER_WATCH 1038 #define IDC_BROWSE_WATCH 1039 #define IDC_LIST_FOLDERS 1040 #define IDC_BUT_SEARCHSUB 1041 #define IDC_BUT_CLEARALL 1042 #define IDC_STATUS1 1043 #define IDC_STATUS2 1044 #define IDC_BUT_NEW 1046 #define IDC_MARK_SPAM_AS_READ 1047 #define IDC_SAVE_SPAM_SCORE 1048 #define IDC_MARK_UNSURE_AS_READ 1051 #define IDC_DELAY1_SLIDER 1056 #define IDC_DELAY1_TEXT 1057 #define IDC_DELAY2_SLIDER 1058 #define IDC_DELAY2_TEXT 1059 #define IDC_INBOX_TIMER_ONLY 1060 #define IDC_VERBOSE_LOG 1061 #define IDB_SBLOGO 1062 #define IDC_LOGO_GRAPHIC 1063 #define IDC_TAB 1068 #define IDC_BACK_BTN 1069 #define IDC_BUT_WIZARD 1070 #define IDC_SHOW_DATA_FOLDER 1071 #define IDC_ABOUT_BTN 1072 #define IDC_BUT_RESET 1073 #define IDC_DEL_SPAM_RS 1074 #define IDC_RECOVER_RS 1075 #define IDC_FORWARD_BTN 1077 #define IDC_PAGE_PLACEHOLDER 1078 #define IDC_BUT_SHOW_DIAGNOSTICS 1080 #define IDC_BUT_PREPARATION 1081 #define IDC_FOLDER_HAM 1083 #define IDC_BUT_UNTRAINED 1088 #define IDC_BUT_TRAIN 1089 #define IDC_BUT_TIMER_ENABLED 1091 #define IDC_WIZ_GRAPHIC 1092 #define IDC_BUT_VIEW_LOG 1093 #define IDC_EDIT1 1094 #define IDC_HAM_SOUND 1094 #define IDC_STATISTICS 1095 #define IDC_UNSURE_SOUND 1095 #define IDC_BUT_RESET_STATS 1096 #define IDC_SPAM_SOUND 1096 #define IDC_LAST_RESET_DATE 1097 #define IDC_ENABLE_SOUNDS 1098 #define IDC_ACCUMULATE_DELAY_SLIDER 1099 #define IDC_ACCUMULATE_DELAY_TEXT 1100 #define IDC_BROWSE_HAM_SOUND 1101 #define IDC_BROWSE_UNSURE_SOUND 1102 #define IDC_BROWSE_HAM_SOUND2 1103 #define IDC_BROWSE_SPAM_SOUND 1103 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 129 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1102 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif spambayes-1.1a6/Outlook2000/dialogs/resources/dialogs.rc0000664000076500000240000007026010646440134023232 0ustar skipstaff00000000000000//Microsoft Developer Studio generated resource script. // #include "dialogs.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" // spambayes dialog definitions ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (U.S.) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Dialog // IDD_ADVANCED DIALOGEX 0, 0, 248, 257 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP CAPTION "Advanced" FONT 8, "Tahoma", 0, 0, 0x1 BEGIN GROUPBOX "Filter timer",IDC_STATIC,7,3,234,117 CONTROL "",IDC_DELAY1_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,16,36,148,22 LTEXT "Processing start delay",IDC_STATIC,16,26,101,8 EDITTEXT IDC_DELAY1_TEXT,165,39,40,14,ES_AUTOHSCROLL LTEXT "seconds",IDC_STATIC,208,41,28,8 CONTROL "",IDC_DELAY2_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,16,73,148,22 LTEXT "Delay between processing items",IDC_STATIC,16,62,142,8 EDITTEXT IDC_DELAY2_TEXT,165,79,40,14,ES_AUTOHSCROLL LTEXT "seconds",IDC_STATIC,207,82,28,8 CONTROL "Only for folders that receive new mail", IDC_INBOX_TIMER_ONLY,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,16,100,217,10 PUSHBUTTON "Show Data Folder",IDC_SHOW_DATA_FOLDER,7,238,70,14 CONTROL "Enable background filtering",IDC_BUT_TIMER_ENABLED, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,16,12,162,10 PUSHBUTTON "Diagnostics...",IDC_BUT_SHOW_DIAGNOSTICS,171,238,70,14 END IDD_STATISTICS DIALOG DISCARDABLE 0, 0, 248, 257 STYLE WS_CHILD | WS_CAPTION CAPTION "Statistics" FONT 8, "Tahoma" BEGIN GROUPBOX "Statistics",IDC_STATIC,7,3,241,229 LTEXT "some stats\nand some more\nline 3\nline 4\nline 5", IDC_STATISTICS,12,12,230,204 PUSHBUTTON "Reset Statistics",IDC_BUT_RESET_STATS,178,238,70,14 LTEXT "Last reset:",IDC_STATIC,7,241,36,8 LTEXT "<<>>",IDC_LAST_RESET_DATE,47,241,107,8 END IDD_MANAGER DIALOGEX 0, 0, 275, 308 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "SpamBayes Manager" FONT 8, "Tahoma", 0, 0, 0x1 BEGIN DEFPUSHBUTTON "Close",IDOK,216,287,50,14 PUSHBUTTON "Cancel",IDCANCEL,155,287,50,14,NOT WS_VISIBLE CONTROL "",IDC_TAB,"SysTabControl32",0x0,8,7,258,276 PUSHBUTTON "About",IDC_ABOUT_BTN,8,287,50,14 END IDD_DIAGNOSTIC DIALOGEX 0, 0, 183, 98 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Diagnostics" FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "These advanced options are for diagnostic or debugging purposes only. You should only change these options if specifically asked to, or you know exactly what they mean.", IDC_STATIC,5,3,174,36 LTEXT "Log file verbosity",IDC_STATIC,5,44,56,8 EDITTEXT IDC_VERBOSE_LOG,73,42,40,14,ES_AUTOHSCROLL PUSHBUTTON "View log...",IDC_BUT_VIEW_LOG,129,41,50,14 CONTROL "Save Spam Score",IDC_SAVE_SPAM_SCORE,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,5,63,72,10 PUSHBUTTON "Cancel",IDCANCEL,69,79,50,14,NOT WS_VISIBLE DEFPUSHBUTTON "Close",IDOK,129,79,50,14 END IDD_FILTER DIALOGEX 0, 0, 249, 257 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP CAPTION "Filtering" FONT 8, "Tahoma" BEGIN LTEXT "Filter the following folders as messages arrive", IDC_STATIC,8,4,168,11 CONTROL "Folder names...\nLine 2",IDC_FOLDER_WATCH,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,7,16,177,12 PUSHBUTTON "Browse...",IDC_BROWSE_WATCH,192,14,50,14 GROUPBOX "Certain Spam",IDC_STATIC,7,31,235,82 LTEXT "To be considered certain spam, a message must score at least", IDC_STATIC,12,40,212,10 CONTROL "Slider1",IDC_SLIDER_CERTAIN,"msctls_trackbar32", TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,13,50,165,22 EDITTEXT IDC_EDIT_CERTAIN,184,53,51,14,ES_AUTOHSCROLL LTEXT "and these messages should be:",IDC_STATIC,13,72,107,10 COMBOBOX IDC_ACTION_CERTAIN,12,83,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "to folder",IDC_STATIC,71,85,28,10 CONTROL "Folder names...",IDC_FOLDER_CERTAIN,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,102,83,77,14 PUSHBUTTON "Browse",IDC_BROWSE_CERTAIN,184,83,50,14 GROUPBOX "Possible Spam",IDC_STATIC,6,117,235,84 LTEXT "To be considered uncertain, a message must score at least", IDC_STATIC,12,128,212,10 CONTROL "Slider1",IDC_SLIDER_UNSURE,"msctls_trackbar32", TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,12,137,165,20 EDITTEXT IDC_EDIT_UNSURE,183,141,54,14,ES_AUTOHSCROLL LTEXT "and these messages should be:",IDC_STATIC,12,158,107,10 COMBOBOX IDC_ACTION_UNSURE,12,169,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "to folder",IDC_STATIC,71,172,27,10 CONTROL "(folder name)",IDC_FOLDER_UNSURE,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,102,169,77,14 PUSHBUTTON "&Browse",IDC_BROWSE_UNSURE,184,169,50,14 CONTROL "Mark spam as read",IDC_MARK_SPAM_AS_READ,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,13,100,81,10 CONTROL "Mark possible spam as read",IDC_MARK_UNSURE_AS_READ, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,189,101,10 GROUPBOX "Certain Good",IDC_STATIC,6,206,235,48 LTEXT "These messages should be:",IDC_STATIC,12,218,107,10 COMBOBOX IDC_ACTION_HAM,12,231,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "to folder",IDC_STATIC,71,233,27,10 CONTROL "(folder name)",IDC_FOLDER_HAM,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,102,231,77,14 PUSHBUTTON "&Browse",IDC_BROWSE_HAM,184,231,50,14 END IDD_GENERAL DIALOGEX 0, 0, 253, 257 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP CAPTION "General" FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "SpamBayes Version Here",IDC_VERSION,6,54,242,8 LTEXT "SpamBayes requires training before it is effective. Click on the 'Training' tab, or use the Configuration Wizard to train.", IDC_STATIC,6,67,242,17 LTEXT "Training database status:",IDC_STATIC,6,90,222,8 LTEXT "123 spam messages; 456 good messages\r\nLine2\r\nLine3", IDC_TRAINING_STATUS,6,101,242,27,SS_SUNKEN CONTROL "Enable SpamBayes",IDC_BUT_FILTER_ENABLE,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,6,221,97,11 LTEXT "Certain spam is moved to Folder1\nPossible spam is moved too", IDC_FILTER_STATUS,6,146,242,67,SS_SUNKEN PUSHBUTTON "Reset Configuration...",IDC_BUT_RESET,6,238,84,15 PUSHBUTTON "Configuration Wizard...",IDC_BUT_WIZARD,164,238,84,15 LTEXT "Filter status:",IDC_STATIC,6,135,222,8 CONTROL 1062,IDC_LOGO_GRAPHIC,"Static",SS_BITMAP | SS_REALSIZEIMAGE,0,2,275,52 END IDD_TRAINING DIALOGEX 0, 0, 252, 257 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP CAPTION "Training" FONT 8, "Tahoma", 0, 0, 0x1 BEGIN GROUPBOX "",IDC_STATIC,5,1,243,113 LTEXT "Folders with known good messages.",IDC_STATIC,11,11,131, 11 CONTROL "",IDC_STATIC_HAM,"Static",SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN,11,21,175,12 PUSHBUTTON "&Browse...",IDC_BROWSE_HAM,192,20,50,14 LTEXT "Folders with spam or other junk messages.",IDC_STATIC, 11,36,171,9 CONTROL "Static",IDC_STATIC_SPAM,"Static",SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,11,46,174,12 PUSHBUTTON "Brow&se...",IDC_BROWSE_SPAM,192,46,50,14 CONTROL "Score &messages after training",IDC_BUT_RESCORE,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,11,64,111,10 CONTROL "&Rebuild entire database",IDC_BUT_REBUILD,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,137,64,92,10 CONTROL "Progress1",IDC_PROGRESS,"msctls_progress32",WS_BORDER, 11,76,231,11 PUSHBUTTON "&Start Training",IDC_START,11,91,54,14,BS_NOTIFY LTEXT "training status training status training status training status training status training status training status ", IDC_PROGRESS_TEXT,75,89,149,17 GROUPBOX "Incremental Training",IDC_STATIC,4,117,244,87 CONTROL "Train that a message is good when it is moved from a spam folder back to the Inbox.", IDC_BUT_TRAIN_FROM_SPAM_FOLDER,"Button",BS_AUTOCHECKBOX | BS_MULTILINE | WS_TABSTOP,11,127,204,18 LTEXT "Clicking 'Not Spam' button should",IDC_STATIC,10,148, 115,10 COMBOBOX IDC_RECOVER_RS,127,145,114,54,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP CONTROL "Train that a message is spam when it is moved to the spam folder.", IDC_BUT_TRAIN_TO_SPAM_FOLDER,"Button",BS_AUTOCHECKBOX | BS_MULTILINE | WS_TABSTOP,11,163,204,16 LTEXT "Clicking 'Spam' button should",IDC_STATIC,10,183,104,10 COMBOBOX IDC_DEL_SPAM_RS,127,180,114,54,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP END IDD_FILTER_NOW DIALOGEX 0, 0, 244, 185 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Filter Now" FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Filter the following folders",IDC_STATIC,8,9,168,11 CONTROL "Folder names...\nLine 2",IDC_FOLDER_NAMES,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN,7,20,172, 12 PUSHBUTTON "Browse...",IDC_BROWSE,187,19,50,14 GROUPBOX "Filter action",IDC_STATIC,7,38,230,40,WS_GROUP CONTROL "Perform all filter actions",IDC_BUT_ACT_ALL,"Button", BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,15,49,126,10 CONTROL "Score messages, but don't perform filter action", IDC_BUT_ACT_SCORE,"Button",BS_AUTORADIOBUTTON,15,62,203, 10 GROUPBOX "Restrict the filter to",IDC_STATIC,7,84,230,35,WS_GROUP CONTROL "Unread mail",IDC_BUT_UNREAD,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,15,94,149,9 CONTROL "Mail never previously spam filtered",IDC_BUT_UNSEEN, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,15,106,149,9 CONTROL "Progress1",IDC_PROGRESS,"msctls_progress32",WS_BORDER,7, 129,230,11 LTEXT "Static",IDC_PROGRESS_TEXT,7,144,227,10 DEFPUSHBUTTON "Start Filtering",IDC_START,7,161,52,14 PUSHBUTTON "Close",IDCANCEL,187,162,50,14 END IDD_FOLDER_SELECTOR DIALOGEX 0, 0, 247, 215 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Select Folder" FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "&Folders:",IDC_STATIC,7,7,47,9 CONTROL "",IDC_LIST_FOLDERS,"SysTreeView32",TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | TVS_DISABLEDRAGDROP | TVS_SHOWSELALWAYS | TVS_CHECKBOXES | WS_BORDER | WS_TABSTOP,7,21,172,140 CONTROL "(sub)",IDC_BUT_SEARCHSUB,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,7,167,126,9 LTEXT "(status1)",IDC_STATUS1,7,180,220,9 LTEXT "(status2)",IDC_STATUS2,7,194,220,9 DEFPUSHBUTTON "OK",IDOK,190,21,50,14 PUSHBUTTON "Cancel",IDCANCEL,190,39,50,14 PUSHBUTTON "C&lear All",IDC_BUT_CLEARALL,190,58,50,14 PUSHBUTTON "&New folder",IDC_BUT_NEW,190,77,50,14 END IDD_WIZARD DIALOGEX 0, 0, 384, 190 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "SpamBayes Configuration Wizard" FONT 8, "Tahoma", 0, 0, 0x1 BEGIN PUSHBUTTON "Cancel",IDCANCEL,328,173,50,14 PUSHBUTTON "<< Back",IDC_BACK_BTN,216,173,50,14 DEFPUSHBUTTON "Next>>,Finish",IDC_FORWARD_BTN,269,173,50,14 CONTROL "",IDC_PAGE_PLACEHOLDER,"Static",SS_ETCHEDFRAME,75,4,303, 167 CONTROL 125,IDC_WIZ_GRAPHIC,"Static",SS_BITMAP,0,0,69,190 END IDD_WIZARD_WELCOME DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Welcome to the SpamBayes configuration wizard", IDC_STATIC,20,4,191,14 LTEXT "This wizard will help you configure the SpamBayes Outlook addin. Please indicate how you have prepared for this application.", IDC_STATIC,20,20,255,18 CONTROL "I haven't prepared for SpamBayes at all.", IDC_BUT_PREPARATION,"Button",BS_AUTORADIOBUTTON | BS_TOP | WS_GROUP,20,42,190,11 CONTROL "I have already sorted good messages (ham) and spam messages into folders that are suitable for training purposes.", IDC_STATIC,"Button",BS_AUTORADIOBUTTON | BS_TOP | BS_MULTILINE,20,59,255,18 CONTROL "I would prefer to configure SpamBayes manually.", IDC_STATIC,"Button",BS_AUTORADIOBUTTON | BS_TOP,20,82, 187,12 LTEXT "If you would like more information about training and configuring SpamBayes, click the About button.", IDC_STATIC,20,103,185,20 PUSHBUTTON "About...",IDC_BUT_ABOUT,215,104,60,15 LTEXT "If you cancel the wizard, you can access it again via the SpamBayes Manager, available from the SpamBayes toolbar.", IDC_STATIC,20,137,232,17 END IDD_WIZARD_FINISHED_UNTRAINED DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Congratulations",IDC_STATIC,20,4,247,14 LTEXT "SpamBayes is now configured and ready to start learning about your Spam", IDC_STATIC,20,22,247,16 LTEXT "As SpamBayes has not been trained, all new mail will arrive in your Unsure folder. As each message arrives, you should use the 'Spam' or 'Not Spam' toolbar buttons as appropriate.", IDC_STATIC,20,42,247,27 LTEXT "If you wish to speed up the training process, you can move all the existing Spam from your Inbox to the new Spam folder, then select 'Training' from the SpamBayes manager.", IDC_STATIC,20,83,247,31 LTEXT "As you train, you will find the accuracy of SpamBayes increases.", IDC_STATIC,20,69,247,15 LTEXT "Click Finish to close the wizard.",IDC_STATIC,20,121, 148,9 END IDD_WIZARD_FOLDERS_REST DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN PUSHBUTTON "Browse...",IDC_BROWSE_SPAM,208,85,60,15 LTEXT "Spam and Unsure Folders",IDC_STATIC,20,4,247,14 LTEXT "SpamBayes uses two folders to manage your Spam - a folder where 'certain' spam is stored, and another for unsure messages.", IDC_STATIC,20,20,247,22 LTEXT "If you enter a folder name and it does not exist, it will be automatically created. If you would prefer to select an existing folder, click the Browse button.", IDC_STATIC,20,44,243,24 EDITTEXT IDC_FOLDER_CERTAIN,20,85,179,14,ES_AUTOHSCROLL LTEXT "Unsure messages will be delivered to a folder named", IDC_STATIC,20,105,186,12 EDITTEXT IDC_FOLDER_UNSURE,20,117,177,14,ES_AUTOHSCROLL LTEXT "Spam will be delivered to a folder named",IDC_STATIC,20, 72,137,8 PUSHBUTTON "Browse...",IDC_BROWSE_UNSURE,208,117,60,15 END IDD_WIZARD_FOLDERS_WATCH DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN PUSHBUTTON "Browse...",IDC_BROWSE_WATCH,225,134,50,14 LTEXT "Folders that receive new messages",IDC_STATIC,20,4,247, 14 LTEXT "SpamBayes needs to know what folders are used to receive new messages. In most cases, this will be your Inbox, but you may also specify additional folders to be watched for spam.", IDC_STATIC,20,21,247,25 LTEXT "The following folders will be watched for new messages. Use the Browse button to change the list, or Next if the list of folders is correct.", IDC_STATIC,20,79,247,20 LTEXT "If you use the Outlook rule wizard to move messages into folders, you may like to select these folders in addition to your inbox.", IDC_STATIC,20,51,241,20 EDITTEXT IDC_FOLDER_WATCH,20,100,195,48,ES_MULTILINE | ES_AUTOHSCROLL | ES_READONLY END IDD_WIZARD_FINISHED_UNCONFIGURED DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Configuration cancelled",IDC_STATIC,20,4,247,14 LTEXT "The main SpamBayes options will now be displayed. You must define your folders and enable SpamBayes before it will begin filtering mail.", IDC_STATIC,20,29,247,16 LTEXT "Click Finish to close the wizard.",IDC_STATIC,20,139, 148,9 END IDD_WIZARD_FOLDERS_TRAIN DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN PUSHBUTTON "Browse...",IDC_BROWSE_HAM,208,49,60,15 LTEXT "Training",IDC_STATIC,20,4,247,10 LTEXT "Please select the folders with the pre-sorted good messages and the folders with the pre-sorted spam messages.", IDC_STATIC,20,16,243,16 EDITTEXT IDC_FOLDER_HAM,20,49,179,14,ES_AUTOHSCROLL | ES_READONLY LTEXT "Examples of Spam, or unwanted messages can be found in", IDC_STATIC,20,71,198,8 EDITTEXT IDC_FOLDER_CERTAIN,20,81,177,14,ES_AUTOHSCROLL | ES_READONLY LTEXT "Examples of good messages can be found in",IDC_STATIC, 20,38,153,8 PUSHBUTTON "Browse...",IDC_BROWSE_SPAM,208,81,60,15 LTEXT "If you have not pre-sorted your messages, or already have training information you wish to keep, please select the Back button and indicate you have not prepared for SpamBayes.", IDC_STATIC,20,128,243,26 CONTROL "Score messages when training is complete", IDC_BUT_RESCORE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,20, 108,163,16 END IDD_WIZARD_TRAIN DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Training",-1,20,4,247,14 LTEXT "SpamBayes is training on your good and spam messages.", -1,20,22,247,16 CONTROL "",IDC_PROGRESS,"msctls_progress32",WS_BORDER,20,45,255, 11 LTEXT "(progress text)",IDC_PROGRESS_TEXT,20,61,257,10 END IDD_WIZARD_FINISHED_TRAINED DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Congratulations",IDC_STATIC,20,4,247,14 LTEXT "SpamBayes has been successfully trained and configured. You should find the system is immediately effective at filtering spam.", IDC_TRAINING_STATUS,20,35,247,26 LTEXT "Even though SpamBayes has been trained, it does continue to learn - please ensure you regularly check your Unsure folder, and use the 'Spam' or 'Not Spam' buttons as appropriate.", IDC_STATIC,20,68,249,30 LTEXT "Click Finish to close the wizard.",IDC_STATIC,20,104, 148,9 END IDD_WIZARD_TRAINING_IS_IMPORTANT DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "SpamBayes will not be effective until it is trained.", IDC_STATIC,11,8,191,14 PUSHBUTTON "About Training...",IDC_BUT_ABOUT,209,140,65,15 LTEXT "SpamBayes is a system that learns about good and bad mail based on examples you provide. It comes with no built-in rules, so must have some training information before it will be effective.", IDC_STATIC,11,21,263,30 LTEXT "In this case, SpamBayes will begin by filtering all mail to an 'Unsure' folder. You can then use the 'Spam' and 'Not Spam' buttons to train each message as it arrives. Slowly SpamBayes will learn about your mail.", IDC_STATIC,22,61,252,29 LTEXT "This option will close the wizard, and provide instructions how to sort your mail. You will then be able to configure SpamBayes and have it be immediately effective at filtering your mail", IDC_STATIC,22,106,252,27 LTEXT "For more information, click the About Training button.", IDC_STATIC,11,143,187,12 CONTROL "I want to continue without training, and let SpamBayes learn as it goes", IDC_BUT_UNTRAINED,"Button",BS_AUTORADIOBUTTON | WS_GROUP, 11,50,263,11 CONTROL "I will pre-sort some good and spam messages, and configure SpamBayes later", IDC_BUT_TRAIN,"Button",BS_AUTORADIOBUTTON,11,92,263,11 END IDD_WIZARD_FINISHED_TRAIN_LATER DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Configuration suspended",IDC_STATIC,20,4,247,14 LTEXT "To perform initial training, you should create a folder that contains only examples of good messages, and another that contains only examples of spam.", IDC_STATIC,20,17,247,27 LTEXT "Click Finish to close the wizard.",IDC_STATIC,20,145, 148,9 LTEXT "For examples of good messages, you may like to use your Inbox - however, it is important you remove all spam from this folder before you commence", IDC_STATIC,20,42,247,26 LTEXT "training. If you have too much spam in your Inbox, you may like to create a temporary folder and copy some examples to it.", IDC_STATIC,20,58,247,17 LTEXT "For examples of spam messages, you may like to look through your Deleted Items folder, and your Inbox. However, you will not be able to specify the Deleted Items folder as examples of spam, so you will need to move them to a folder you create.", IDC_STATIC,20,80,247,35 LTEXT "When you are finished, open the SpamBayes Manager via the SpamBayes toolbar, and re-start the Configuration Wizard.", IDC_STATIC,20,121,245,17 END IDD_NOTIFICATIONS DIALOGEX 0, 0, 248, 257 STYLE WS_CHILD | WS_CAPTION CAPTION "Notifications" FONT 8, "Tahoma" BEGIN GROUPBOX "New Mail Sounds",IDC_STATIC,7,3,241,229 CONTROL "Enable new mail notification sounds",IDC_ENABLE_SOUNDS, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,14,17,129,10 LTEXT "Good sound:",IDC_STATIC,14,31,42,8 EDITTEXT IDC_HAM_SOUND,14,40,174,14,ES_AUTOHSCROLL PUSHBUTTON "Browse...",IDC_BROWSE_HAM_SOUND,192,40,50,14 LTEXT "Unsure sound:",IDC_STATIC,14,58,48,8 EDITTEXT IDC_UNSURE_SOUND,14,67,174,14,ES_AUTOHSCROLL PUSHBUTTON "Browse...",IDC_BROWSE_UNSURE_SOUND,192,67,50,14 LTEXT "Spam sound:",IDC_STATIC,14,85,42,8 EDITTEXT IDC_SPAM_SOUND,14,94,174,14,ES_AUTOHSCROLL PUSHBUTTON "Browse...",IDC_BROWSE_SPAM_SOUND,192,94,50,14 LTEXT "Time to wait for additional messages:",IDC_STATIC,14, 116,142,8 CONTROL "",IDC_ACCUMULATE_DELAY_SLIDER,"msctls_trackbar32", TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,14,127,148,22 EDITTEXT IDC_ACCUMULATE_DELAY_TEXT,163,133,40,14,ES_AUTOHSCROLL LTEXT "seconds",IDC_STATIC,205,136,28,8 END ///////////////////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO MOVEABLE PURE BEGIN IDD_ADVANCED, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 241 VERTGUIDE, 16 BOTTOMMARGIN, 204 END IDD_MANAGER, DIALOG BEGIN BOTTOMMARGIN, 253 END IDD_DIAGNOSTIC, DIALOG BEGIN LEFTMARGIN, 5 RIGHTMARGIN, 179 BOTTOMMARGIN, 93 END IDD_FILTER, DIALOG BEGIN BOTTOMMARGIN, 254 HORZGUIDE, 127 END IDD_GENERAL, DIALOG BEGIN RIGHTMARGIN, 248 VERTGUIDE, 6 BOTTOMMARGIN, 205 END IDD_TRAINING, DIALOG BEGIN RIGHTMARGIN, 241 VERTGUIDE, 11 VERTGUIDE, 242 BOTTOMMARGIN, 207 END IDD_FILTER_NOW, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 237 TOPMARGIN, 9 BOTTOMMARGIN, 176 END IDD_WIZARD, DIALOG BEGIN RIGHTMARGIN, 378 END IDD_WIZARD_WELCOME, DIALOG BEGIN VERTGUIDE, 20 VERTGUIDE, 275 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_UNTRAINED, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FOLDERS_REST, DIALOG BEGIN VERTGUIDE, 20 VERTGUIDE, 268 BOTTOMMARGIN, 161 HORZGUIDE, 85 HORZGUIDE, 117 END IDD_WIZARD_FOLDERS_WATCH, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_UNCONFIGURED, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FOLDERS_TRAIN, DIALOG BEGIN VERTGUIDE, 20 VERTGUIDE, 268 BOTTOMMARGIN, 161 HORZGUIDE, 49 HORZGUIDE, 81 END IDD_WIZARD_TRAIN, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_TRAINED, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_TRAINING_IS_IMPORTANT, DIALOG BEGIN VERTGUIDE, 11 VERTGUIDE, 22 VERTGUIDE, 274 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_TRAIN_LATER, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_NOTIFICATIONS, DIALOG BEGIN LEFTMARGIN, 7 TOPMARGIN, 7 BOTTOMMARGIN, 232 END END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Bitmap // IDB_SBLOGO BITMAP MOVEABLE PURE "sblogo.bmp" IDB_SBWIZLOGO BITMAP MOVEABLE PURE "sbwizlogo.bmp" IDB_FOLDERS BITMAP MOVEABLE PURE "folders.bmp" #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE MOVEABLE PURE BEGIN "dialogs.h\0" END 2 TEXTINCLUDE MOVEABLE PURE BEGIN "#include ""winres.h""\r\n" "// spambayes dialog definitions\r\n" "\0" END 3 TEXTINCLUDE MOVEABLE PURE BEGIN "\0" END #endif // APSTUDIO_INVOKED #endif // English (U.S.) resources ///////////////////////////////////////////////////////////////////////////// spambayes-1.1a6/Outlook2000/dialogs/resources/folders.bmp0000664000076500000240000000547610646440134023427 0ustar skipstaff00000000000000BM> v(j wwwwwww wwwwwww wwwwwwpwwwwwwp   {     { pp    wp { pp  𙙙  𪪪 |  { pp  𙙙  𪪪 | { xpp  " | { wwwwww{pwwwwww p   """"" ff |  { p p  """"" ff |  p pp  """ ffwp {www{ ww|w|0    "" "/""  ff ǸǸ|    ww|w wwww   spambayes-1.1a6/Outlook2000/dialogs/resources/rc2py.py0000664000076500000240000000317210646440134022671 0ustar skipstaff00000000000000# rc2py.py # This module is part of the spambayes project, which is Copyright 2003 # The Python Software Foundation and is covered by the Python Software # Foundation license. __author__="Adam Walker" __doc__="""" Converts an .rc windows resource source file into a python source file with the same basic public interface as the rcparser module. """ import sys, os, stat import rcparser def convert(inputFilename = None, outputFilename = None, enableGettext = True): """See the module doc string""" if inputFilename is None: inputFilename = "dialogs.rc" if outputFilename is None: outputFilename = "test.py" rcp = rcparser.ParseDialogs(inputFilename, enableGettext) in_stat = os.stat(inputFilename) out = open(outputFilename, "wt") out.write("#%s\n" % outputFilename) out.write("#This is a generated file. Please edit %s instead.\n" % inputFilename) out.write("_rc_size_=%d\n_rc_mtime_=%d\n" % (in_stat[stat.ST_SIZE], in_stat[stat.ST_MTIME])) out.write("try:\n _\nexcept NameError:\n def _(s):\n return s\n") out.write("class FakeParser:\n") out.write(" dialogs = "+repr(rcp.dialogs)+"\n") out.write(" ids = "+repr(rcp.ids)+"\n") out.write(" names = "+repr(rcp.names)+"\n") out.write(" bitmaps = "+repr(rcp.bitmaps)+"\n") out.write("def ParseDialogs(s):\n") out.write(" return FakeParser()\n") out.close() if __name__=="__main__": if len(sys.argv) > 3: convert(sys.argv[1], sys.argv[2], bool(int(sys.argv[3]))) elif len(sys.argv) > 2: convert(sys.argv[1], sys.argv[2], True) else: convert() spambayes-1.1a6/Outlook2000/dialogs/resources/rclabels2text.py0000664000076500000240000000277710646440134024422 0ustar skipstaff00000000000000# rclabels2text.py # This module is part of the spambayes project, which is Copyright 2003 # The Python Software Foundation and is covered by the Python Software # Foundation license. __author__="Adam Walker" __doc__="""" Pulls labels and captions out of a windows resource file and writes them into a text file for spell checking purposes. """ import sys, os, re import rcparser anti_and = re.compile(r"([^\\]*)&([^&]*)"); anti_nl = re.compile(r"([^\\]*)\\n([^\\])"); def extract(inputFilename = None, outputFilename = None): """See the module doc string""" if inputFilename is None: inputFilename = "dialogs.rc" if outputFilename is None: outputFilename = "spellcheck.txt" rcp = rcparser.ParseDialogs(inputFilename) out = open(outputFilename, "wt") for dlg_id in rcp._dialogs: print dlg_id dlg = rcp._dialogs[dlg_id] out.write("\n================================================\n") out.write("In Dialog: "+str(dlg_id)+" Title: "+str(dlg.caption)+"\n\n") for ctrl in dlg.controls: if len(ctrl.label)>0: out.write(ctrl.id) out.write("\n") s = ctrl.label s = anti_and.sub(r"\g<1>\g<2>", s) s = anti_nl.sub("\\g<1>\n\\g<2>",s) out.write(s) out.write("\n\n") out.close() os.startfile(outputFilename); if __name__=="__main__": if len(sys.argv)>1: extract(sys.argv[1], sys.argv[2]) else: extract() spambayes-1.1a6/Outlook2000/dialogs/resources/rcparser.py0000664000076500000240000003275111116562777023473 0ustar skipstaff00000000000000# Windows dialog .RC file parser, by Adam Walker. # This module is part of the spambayes project, which is Copyright 2003 # The Python Software Foundation and is covered by the Python Software # Foundation license. __author__="Adam Walker" import sys, os, shlex import win32con #import win32gui import commctrl _controlMap = {"DEFPUSHBUTTON":0x80, "PUSHBUTTON":0x80, "Button":0x80, "GROUPBOX":0x80, "Static":0x82, "CTEXT":0x82, "RTEXT":0x82, "LTEXT":0x82, "LISTBOX":0x83, "SCROLLBAR":0x84, "COMBOBOX":0x85, "EDITTEXT":0x81, } _addDefaults = {"EDITTEXT":win32con.WS_BORDER, "GROUPBOX":win32con.BS_GROUPBOX, "LTEXT":win32con.SS_LEFT, "DEFPUSHBUTTON":win32con.BS_DEFPUSHBUTTON, "CTEXT":win32con.SS_CENTER, "RTEXT":win32con.SS_RIGHT} defaultControlStyle = win32con.WS_CHILD | win32con.WS_VISIBLE class DialogDef: name = "" id = 0 style = 0 styleEx = None caption = "" font = "MS Sans Serif" fontSize = 8 x = 0 y = 0 w = 0 h = 0 template = None def __init__(self, n, i): self.name = n self.id = i self.styles = [] self.stylesEx = [] self.controls = [] #print "dialog def for ",self.name, self.id def createDialogTemplate(self): t = None self.template = [[self.caption, (self.x,self.y,self.w,self.h), self.style, self.styleEx, (self.fontSize, self.font)]] # Add the controls for control in self.controls: self.template.append(control.createDialogTemplate()) return self.template class ControlDef: id = "" controlType = "" subType = "" idNum = 0 style = defaultControlStyle label = "" x = 0 y = 0 w = 0 h = 0 def __init__(self): self.styles = [] def toString(self): s = "" return s def createDialogTemplate(self): ct = self.controlType if "CONTROL"==ct: ct = self.subType if ct in _addDefaults: self.style |= _addDefaults[ct] if ct in _controlMap: ct = _controlMap[ct] t = [ct, self.label, self.idNum, (self.x, self.y, self.w, self.h), self.style] #print t return t class gt_str(str): """Change a string to a gettext version of itself.""" def __repr__(self): if len(self) > 0: # timeit indicates that addition is faster than interpolation # here return "_(" + super(gt_str, self).__repr__() + ")" else: return super(gt_str, self).__repr__() class RCParser: next_id = 1001 dialogs = {} _dialogs = {} debugEnabled = False; token = "" def __init__(self): self.ids = {"IDOK":1, "IDCANCEL":2, "IDC_STATIC": -1} self.names = {1:"IDOK", 2:"IDCANCEL", -1:"IDC_STATIC"} self.bitmaps = {} self.gettexted = False def debug(self, *args): if self.debugEnabled: print args def getToken(self): self.token = self.lex.get_token() self.debug("getToken returns:", self.token) if self.token=="": self.token = None return self.token def getCommaToken(self): tok = self.getToken() assert tok == ",", "Token '%s' should be a comma!" % tok def loadDialogs(self, rcFileName): """ RCParser.loadDialogs(rcFileName) -> None Load the dialog information into the parser. Dialog Definations can then be accessed using the "dialogs" dictionary member (name->DialogDef). The "ids" member contains the dictionary of id->name. The "names" member contains the dictionary of name->id """ hFileName = rcFileName[:-2]+"h" if not os.path.exists(hFileName): # Translated dialogs don't need their own copy of dialogs.h, # so look for one in this directory if there isn't one in the # expected place. # This will only work with Python > 2.2 and as source, but # it shouldn't ever be run by binary users, so that shoudln't # matter. hFileName = os.path.join(os.path.dirname(__file__), os.path.basename(hFileName)) try: h = open(hFileName, "rU") self.parseH(h) h.close() except IOError: print "No .h file. ignoring." f = open(rcFileName) self.open(f) self.getToken() while self.token!=None: self.parse() self.getToken() f.close() def open(self, file): self.lex = shlex.shlex(file) self.lex.commenters = "//#" def parseH(self, file): lex = shlex.shlex(file) lex.commenters = "//" token = " " while token is not None: token = lex.get_token() if token == "" or token is None: token = None else: if token=='define': n = lex.get_token() i = int(lex.get_token()) self.ids[n] = i if self.names.has_key(i): # ignore AppStudio special ones. if not n.startswith("_APS_"): print "Duplicate id",i,"for",n,"is", self.names[i] else: self.names[i] = n if self.next_id<=i: self.next_id = i+1 def parse(self): deep = 0 if self.token == None: more == None elif "BEGIN" == self.token: deep = 1 while deep!=0: self.getToken() if "BEGIN" == self.token: deep += 1 elif "END" == self.token: deep -= 1 elif "IDD_" == self.token[:4]: possibleDlgName = self.token #print "possible dialog:", possibleDlgName self.getToken() if "DIALOG" == self.token or "DIALOGEX" == self.token: self.dialog(possibleDlgName) elif "IDB_" == self.token[:4]: possibleBitmap = self.token self.getToken() if "BITMAP" == self.token: self.getToken() if self.token=="MOVEABLE": self.getToken() # PURE self.getToken() # bmpname bmf = self.token[1:-1] # quotes self.bitmaps[possibleBitmap] = bmf print "BITMAP", possibleBitmap, bmf #print win32gui.LoadImage(0, bmf, win32con.IMAGE_BITMAP,0,0,win32con.LR_DEFAULTCOLOR|win32con.LR_LOADFROMFILE) def addId(self, id_name): if id_name in self.ids: id = self.ids[id_name] else: id = self.next_id self.next_id += 1 self.ids[id_name] = id self.names[id] = id_name return id def lang(self): while self.token[0:4]=="LANG" or self.token[0:7]=="SUBLANG" or self.token==',': self.getToken(); def dialog(self, name): dlg = DialogDef(name,self.addId(name)) assert len(dlg.controls)==0 self._dialogs[name] = dlg extras = [] self.getToken() while not self.token.isdigit(): self.debug("extra", self.token) extras.append(self.token) self.getToken() dlg.x = int(self.token) self.getCommaToken() self.getToken() # number dlg.y = int(self.token) self.getCommaToken() self.getToken() # number dlg.w = int(self.token) self.getCommaToken() self.getToken() # number dlg.h = int(self.token) self.getToken() while not (self.token==None or self.token=="" or self.token=="END"): if self.token=="STYLE": self.dialogStyle(dlg) elif self.token=="EXSTYLE": self.dialogExStyle(dlg) elif self.token=="CAPTION": self.dialogCaption(dlg) elif self.token=="FONT": self.dialogFont(dlg) elif self.token=="BEGIN": self.controls(dlg) else: break self.dialogs[name] = dlg.createDialogTemplate() def dialogStyle(self, dlg): dlg.style, dlg.styles = self.styles( [], win32con.WS_VISIBLE | win32con.DS_SETFONT) def dialogExStyle(self, dlg): self.getToken() dlg.styleEx, dlg.stylesEx = self.styles( [], 0) def styles(self, defaults, defaultStyle): list = defaults style = defaultStyle if "STYLE"==self.token: self.getToken() i = 0 Not = False while ((i%2==1 and ("|"==self.token or "NOT"==self.token)) or (i%2==0)) and not self.token==None: Not = False; if "NOT"==self.token: Not = True self.getToken() i += 1 if self.token!="|": if self.token in win32con.__dict__: value = getattr(win32con,self.token) else: if self.token in commctrl.__dict__: value = getattr(commctrl,self.token) else: value = 0 if Not: list.append("NOT "+self.token) self.debug("styles add Not",self.token, value) style &= ~value else: list.append(self.token) self.debug("styles add", self.token, value) style |= value self.getToken() self.debug("style is ",style) return style, list def dialogCaption(self, dlg): if "CAPTION"==self.token: self.getToken() self.token = self.token[1:-1] self.debug("Caption is:",self.token) if self.gettexted: # gettext captions dlg.caption = gt_str(self.token) else: dlg.caption = self.token self.getToken() def dialogFont(self, dlg): if "FONT"==self.token: self.getToken() dlg.fontSize = int(self.token) self.getCommaToken() self.getToken() # Font name dlg.font = self.token[1:-1] # it's quoted self.getToken() while "BEGIN"!=self.token: self.getToken() def controls(self, dlg): if self.token=="BEGIN": self.getToken() while self.token!="END": control = ControlDef() control.controlType = self.token; #print self.token self.getToken() if self.token[0:1]=='"': if self.gettexted: # gettext labels control.label = gt_str(self.token[1:-1]) else: control.label = self.token[1:-1] self.getCommaToken() self.getToken() elif self.token.isdigit(): control.label = self.token self.getCommaToken() self.getToken() # msvc seems to occasionally replace "IDC_STATIC" with -1 if self.token=='-': if self.getToken() != '1': raise RuntimeError, \ "Negative literal in rc script (other than -1) - don't know what to do" self.token = "IDC_STATIC" control.id = self.token control.idNum = self.addId(control.id) self.getCommaToken() if control.controlType == "CONTROL": self.getToken() control.subType = self.token[1:-1] # Styles self.getCommaToken() self.getToken() control.style, control.styles = self.styles([], defaultControlStyle) #self.getToken() #, # Rect control.x = int(self.getToken()) self.getCommaToken() control.y = int(self.getToken()) self.getCommaToken() control.w = int(self.getToken()) self.getCommaToken() self.getToken() control.h = int(self.token) self.getToken() if self.token==",": self.getToken() control.style, control.styles = self.styles([], defaultControlStyle) #print control.toString() dlg.controls.append(control) def ParseDialogs(rc_file, gettexted=False): rcp = RCParser() rcp.gettexted = gettexted try: rcp.loadDialogs(rc_file) except: lex = getattr(rcp, "lex", None) if lex: print "ERROR parsing dialogs at line", lex.lineno print "Next 10 tokens are:" for i in range(10): print lex.get_token(), print raise return rcp if __name__=='__main__': rc_file = os.path.join(os.path.dirname(__file__), "dialogs.rc") d = ParseDialogs(rc_file) import pprint for id, ddef in d.dialogs.items(): print "Dialog %s (%d controls)" % (id, len(ddef)) pprint.pprint(ddef) print spambayes-1.1a6/Outlook2000/dialogs/resources/sblogo.bmp0000664000076500000240000007320210646440134023246 0ustar skipstaff00000000000000BMv6(|MLrʦ     "&4+ 7 +$$6('80/:65AD!E*'V*'F0.U1.G87U:8b-)h1,p3.f;7w:5I@>V@>cA>|@;JDCWHF[PNZTSeIGxJFePNtPMfWVvYWk`^w`^jddwihwpoxts=7doqB<C=r5IDKEQMQK[W[VLENFSLTLZS\Ta^a]b\b[igjfpnqmwvwulgjcqmrmyu{vOENDPEWNULYORGSH\S\R^TYM]R`Wb[c[h_aXaVdZdYh^h]ialdohkbunrkskwrwpzuzs~y~xxpkblbpfsjtjxoxo|t{rx=~~}}Ă}˂}ڃ|{ĆʅÉʊÎʎ֋Ñˑ֒Õʕęʙʝؚ狃擌囕Šˡء墜ĥʤĨ̩Ƭ˭ةŰ˰رŴ˴ĸ͸¼˼ٹ䪥㱬⺶ ````_aaefffcfcccccdddddhhhilhllllnnlnnnonooooowowwwwxwxxxx{x{{~~~~~~~~ݸ߶jjjjjjkkkkkkqqqsqssssuuuuuu̖̖߸jjjjjkjkkkkqkqqsqsssusuuuu̖̖ƽ߸Wjjjjjjkkkkkkqqqsqssssuuuuuu̖̖˿][ⶵW;;;:;:;:;:;;;;;;;jjjjjkjkkkkqkqqsqsssusuuuu̖̖ƽ]VTVⶰW;;;::9:9:8:8:8:9:8;::::;;;jjjjjjkkkkkkqqqsqssssuuuuuu̖̖̕]]]]TTT]ÿǪϬӳ߲W;;:=================9:8:8:::::;;;;jjjjjkjkkkkqkqqsqsssusuuuu̖̖̔\\\^}\[\\^\]^^^^]]^]^]^Ľ^]3)]]TSTV[Ɵ^]ơšǣŪǣɪɪӪӱӬ⵰W=>==>===================>=>8:8::8;::;;;;jjjjjjkkkkkkqqqsqssssuuuuuu̖̖̕}YYQ\\\YYYYYYYY^^^[Y[[[TY[[VYV^][T0 "]]TSTTV[]¿[XVVVVV]]]]]Ş]]XXXXX]Ū⵩>==>===========================BB=>8:8::8;::;jjjjjkjkkkkqkqqsqsssusuuuu̖̖̖̕}ZPPQYZ[YPPPYYYPT[^^^YY[]^^]YPTT[[[TY[]VT0 )[[VTSTVVT[[TTUTVSTV]]VV]XV]X]śX]X]XXXUXXXXXXXXXXXXX=========== :8::;;jjjjjjkkkkkkqqqsqssssuuuuuù̖̖EEEHz\QE*0*1E**0GYT0*13\]YOE333GO*01G[[[T[ý]TG5GOYVT05335V[[VT5335O2003SO000S]V3237O223XX]ʞX7547US554]X754575222UX95447SXX97657UXU42499445/46UWU96699WX9464> /======::8;::;jjjjjkjkkkkqkqqsqsssusuuuu̖̖zEzY)*[PY]P0![]YV[^G!PT23]]V)O-0XTX])5]١2!S(S4SS!9XU79,9U(U,-W(9>>== B8:8::;;jjjjjjkkkkkkqqqsqssssuuuuuu̖̖̖*zYP **[PT\00"[^[[[^^]0!T3OOO00]SX])7Xў)-)US7XSUS)9S(,U,9>=>=B>=::8;;;jjjjjkjkkkkqkqqsqsssusuuuu̖̖̕1"&1*0\PTY*")1"]^[[]^]0*S3""]5"O00]SV]-5]3-(--XV(!95!)!9S-)7U!-9U)6,.,!:-9>=======>8:::;jjjjjjkkkkkkqqqsqssssuuuuuu̖ZP*3\TTT*0Y[5)[[TTTY[]^VG5*T3)T*]5)VSO03TVX-5]X!2VXV7)-0XX!VU!SU45-!UW57.!7U!-SU,.UWS44W-U=========:::;jjjjjkjkkkkqkqqsqsssusuuuù̖̖̖"P01^PPT*5\]G)P*!"^5[30]5VO23O23VVX05S]2X-5!S7X9!!UU(.9S-W-(((!-W-U===========:;;jjjjjjkkkkkkqqqsqssssuuuuuu̖̕E"&EY13}GPY0G^]G)O)]!!O]50^OV[2!!S35O!VV)!7O!!0ǣ2!!V7!!!!!U!!!!!7!!!!5W!!(2!SU!-W-!!!-W.!W=============;jjjjjkjkkkkqkqqsqsssusuuuu̖̖̖Z "& YY1 EE"Y]0"G^^G"*T"^T"""3][^7"33!"!S"!"!!T5"57(!"XV-!"O5!)!SŠǞ2!)VU-!)(XX()!(4()((2!((-4!(!SU(!..!,!..!,(.4((!Wլ===================jjjjjjkkkkkkqqqsqssssuuuuuu̕z ""  " "\\3 ""0 """ Y*""!0GOG""0[[YT[[]P!)"!33![O!)"!0""))!ÑTVS5!-)!7!))!2!"))"-0!)-XO(-"Oʣś2"-!U‘SUS5(!--X-)(25-W--!25-S(----(-(4US!-(/6!-,2.--,66,.,!6W====================jjjjjkjkkkkqkqqsqsssusuuuu{|~{~~~~~~z1 &"&P\E!&*""***)*G***"!3*"0^^"*00*)0"]T"0*))000"3G")*000)0S"00))000-")-000!V(20-ţ2--!VS)(-0.2-(٪"-2.-0.)(-2..-2,X)..27(..-,7(.-2.).(!2/-W7./.-.,,=================jjjjjjkkkkkkqqqsqssssuuuuuu{vZZRZRZZZZZZZZZZ\E&" **^E"*" )&*)*TG)010** 3!*)1O00*-**-T"0)"*-*)*S)00-*-*0S"0)"-0--*70-*0-5X)34"XV402)XU)-0.0-.0X22.2.0-2.2.2.-,2.29-2442(7(.-4W.-...-U9-4-,.2.հ>============>=jjjjjkjkkkkqkqqsqsssusuuuu|vRHFHFHFHFHHHHHHHHE&1110&P131PTGGGTT*351030T033O]POOGO[^353STOO5ђTTSG7OO]555VVSOOSO7T25735S-4]545-XОSSS77SSܣUUSSUXXUSUUX7779X-674.4X4659ӐS99Uސ796W:9ש=>=========>=jjjjjjkkkkkkqqqsqssssuuuuuu|ZMDECECECECECEECEEE&"EHH*YλGGP3O[][V5SV77S757O9OU5ܢ֪97U:-XբWW:W:W;UW>=======>jjjjjkjkkkkqkqqsqsssusuuuu{RHCC1+11C1111111C1E1&HYY&OEO3Y^GTOTS7]VSVXSUXWXWSWX47WUౙW:999999:UW=>>=>=>jjjjjjkkkkkkqqqsqssssuuuuuu'111E1&GHH3^YYT]P[TX75SUS7SXXUWUɬ46X(!.999:U?>=jjjjjkjkkkkqkqqsqsssusuuuu| 11CEHEHYQ[P^YTŠVSUX9S-4X7 999:U򘨩 == jjjjjjkkkkkkqqqsqssssuuuuuu| 111EPZ]]]٣ܛӛ7 /9:9U;??>=================>??jjjjjkjkkkkqkqqsqsssusuuuu| 11EEP\9 9:9:WW=================jjjjjjkkkkkkqqqsqssssuuuuuuttt| 1CEHQ\8 /:U:UWjjjjjkjkkkkqkqqsqsssusuuuutrppnpnpnopyyyyyyyyyy|||       1EHHZ\}6     (9;U:WjjjjjjkkkkkkqqqsqssssuuuutrlgbLLLLLNgmlmggNNRNmmvy|x  1EHPQZZ\\\^z}ľ^^^^^^^¡šţǪӬ6   4;W:WWݶᶴjjjjjkjkkkkqkqqsqsssusuuutpgLIIDIDIIKIKKKKKFKKKMNRvv  1EHHPQPQPQQYYY\ľ^^[[YTYT[[[[][[VTTYVTVTV[VYV[]][VVYVV[V[V[VV[V[]]X[VV[VVX[VX[XX]]XXVXXX]XXX]X]X]XXXXXXXXXXXXɪ4  (:UW;WݵⷩjjjjjjkkkkkkqqqsqssssuuuuribIAAAAA@AADDDDADDCDDDFMMR  1EEHHGHHGHGHHPTY\ļ^[YTPTOPOPOPSPTOPOOOOOOOPOOOOTSY]]TSOOSOSOSOSOSSSSOTV]]VTSSSSSTSSSSSSTV]VVSSUSTSSSUSUSVXXUSUUUUUUUUUUUUUUUUUUUUUXUXXXӪXWXWWWWWXWXXWUWWWWWWWWWWWX9  6W;WWݵᮚjjjjjkjkkkkqkqqsqsssusuuusiLIA@@@@@@@A@CAC@CCACCDDFMCCF  & &EEEEEEEGEGEGHGHPTY}ļ^[TPOOGOGOGOGGOGGGGGOGOGOOOOOOGOT[][TOOOOOOOOOOOOOOOOSTV[VSOOSOSOSOSOSOSTX]XVSSSOSSSSSSSSSSXUSSSSSSSSSSSSSSSUSSUSUSUSUUUUUXXXժWWUUUUUUUUUWUU;UUUWWUWWUWU;UW  ,;WWWڰWWWW칫jjjjjjkkkkkkqqqsqssssr%%@'#'C@CCDCDDFMRx  E1&  0EEGGEGGHGTY^^O1""03G!O3"O!27S3!).59SUSUUUUUXUܪW7-!!-/7U-UWUW 9WWWWඦ9/(,./;;WW-,,,(//,,,8:jjjjjkjkkkkqkqqsqsssup  $CCCCCDEFMR   1GGGHGGHP[^^*  O3 O *5 S- 4SUUUUUUXX4 UUWW (WWWW6 6;W ,8jjjjjjkkkkkkqqqsqssssr   +DCDCDEFR   EGGHGGPP[G   O0  O  -5 S0 -UUUUXUWU   :WUW!  :WWW/  4W   jjjjjkjkkkkqkqqsqsssup   'DCDCDFM   1HOHOGP3   O0  O  -5 S-  SWUUUXS   UUWW (;WW  ;  jjjjjjkkkkkkqqqsqssssr  'DDEDFF  *HPGP3  O0  O  *5 S-  7XWUU  :UW  :WW ;  jjjjjkjkkkkqkqqsqsssup  CDFDCF  3PPG   O3 O  -5S0 !UWX.   UW: (WX      ;jjjjjjkkkkkkqqqsqssssr  #DFDEF  GPO    P2  O  -G V0 4XX    :X 9ݰ    /    ,jjjjjkjkkkkqkqqsqsssup   CFFDF   EPG  !GPO0 S3  S  07U2  (UW !UWU7 U9  (-(;W(  ;  ,8:/, jjjjjjkkkkkkqqqsqssssr    +FFDF    *PG  GTOPO T3  S  07 X2    SX UWXWW! U   : : ,/6;(, jjjjjkjkkkkqkqqsqsssup  'DI@  &FFFF  EEHE  PP  GOPOP  !""TO33" )*2S (007O 225V7  !)("! 7X  SXWXW! -,W7   ,6 ( 6,/68:;//884  jjjjjjkkkkkkqqqsqssssr  8>8<5>6>8?8?8cghjilss@:A;D=B:D<F>F=w:x<F@G@HAHBG@KDJBOHOHLDLCOHPIPIPGPFTLRJYQ||MDPFQGSITKXOXOTJVKYOXN]U]U[Q\RYN\Q\Q`WaX`V`Vd[d[h^`VaUcXf[h^i`mclapfpfriukxn|r?@v{ĵȷľɼʶʶ̿˶llllllllllllllllϵHHHHHHHHHHHHHHHHH;;;;;lllllпJJJJJHHHHHHHHHHHHHHHHHJJJJJ;;;;l;lll϶LLJJJHHHHHHHHHHHHHHHHHHHHHHHHHJXXJL;;;;;;lllLHHHHHHHHHH ;;;llN 8HHHHHH::::;llLNHH W::::;llMKMHWJJ999;lMKHHHHHII99:lMHHHHHHHI9:;lHHHHHHHHHHH;lHHHHHHHHHHHHHlHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHKHHHHHHHHHHHHHHHLKKHHHHHHHHHHHKLKKKHHHHHHHKLKKKHHHHHKKKMOHKKMOKK森 HH OOKHHHHHHHHHHHHHHHHHNNNHHHHHHHHHHHHHHHHHxxxxifeeeiki`[RR[[exf\QPPPQ\ff[PA7APR^kQAAAAQ\fyxxxxxkxxxxxxxyyxxxxxxxxkx6QAAAPY`kxxxxxxkxxxxxxifaaaaafeaaffffiikxxkiffffeaaaaaf RPPPQQ\iyxkiiffffiikxxii``````afiyi^Z[[[[Y[YYYY[Z\\^`fkxyki^^[Z[YYRRYY[[ 7QQPQQY`kkd`\\[YYY\\`fkkki`ZYRRQRQRY\xfZRQRRQRQQQQQRQRRYY\`iiia\YRRQQQPPPQRQQ QQQQQR\iyyi`\YRQQQQQQQR\^ff`YQPPAAAAAPQxaYQQRQRQQQQQPQQRQRRY[\^^[YRRQQQAAAAPQRQAQQQPQYakia\RRQQPPPPPPQQY\^\RPA77777AAQ 1PQRRRRR)RRQRA QQPPQR^ixB% !1PPPA! '7AAPQ.RRY6 YRRRAQAABQZ^62AA!1QQQ $YA  YYR?$PAABQR  'A! 6QQ7 ZYY AAAAQ  4$ PB   \Z7'A?A1  AA  2\Z 7AARR%)17 7AZ!!!! ^R !  4\Z4  (AAQ$  AA5%$$$\R  Q 4ZZ! !!?A !-!!!!! 7AB5''%% !!!!7^R$!!!4R. $%$!?! )A!$$!$!%%  ABQQ5(((%  %$ 71(!  %%%%?''%%%%'$^%%%1%(!QRZ5-)((''((((' f2(((( Z)))( !'.'()(((^ %)\_a5.-)(!%($ Bj$')& ---- r.....------Z.-.) %.)..1 )-drr51.-)!-))(dd%'---'! Z22121111111^% !'-&$ $d%)-' -1r4421.%dQ ..... Yb^1 Qd\ZZ yB?54(44442BdgjjY-)ddS.!%2_.!zZRCA12?A?B? ^prygdd_^TQ..55ZTZTZZzyrrrpjjjjgj^]B?4-fd'-_{{{{{zsrppppppppsz{rb_]]]]]]]_gpzvh]SEBCBCBEE]gssbS@???????@S_q{sbF@>5>>>5>?EUpz{{{{{{{{&>>7>CTgs{{{{{{{{{{{{z{{z{{{{{{z{{z{{{z{{{{z{{z{{z{{{{{vqpppppppp%>>>>@SUhpsv{{vqpppppppppppppppphpppppppppphppppppppppppppppppqznb_UUUUUU_" =?>>?@S]_bhqv{{vphb_UUUUUUUUUUUU]T]U]UUUUUUU]U]TUUUUUU]T]TUUUUUU_b{sbTFEEEECEE!   @?>>?@@ESSUbpszzqn_USSFEEECEEEEEEEC@@CCEEEFEEE@@@CEEEEEECC@CCEEEEEFT{o_FC@?@?=@?@@?=>=@@CEFS_hqqh_GECC@@?=?=@@@@@?>>>>@@@@@@@?=>>?@@@@@@>>>>?@@C@@@Fwp_F@@?=>?>?@@@?=?@@@CCET_cgVTEC@@@@>>>?@@@@@>>>5>?@@@C@??><>>?@@@@?>>>>?@@@@@@E 3?=?= /@@@CCCCC+ C@@@F"@@C3CCEE3 ECCCT  &CE  >CF>   ECEF_ >D CF  FEDTbF+F    FFFUc<EG /  SDSUn CC3 S  0FC,0FG" =SSDG_oCG@ D@ 0FF0 &< @ =SDSTco@""=3,GTF0SC,"" @DGTVhtG""""""" """"$"""#3,*" """"1: idd = sys.argv[1] if idd=='IDD_WIZARD': ShowWizard(0, mgr, idd) else: ShowDialog(0, mgr, mgr.config, idd) if "-d" in sys.argv: print "Dumping(but not saving) new manager configuration:" print mgr.options.display() print "-- end of configuration --" mgr.Close() spambayes-1.1a6/Outlook2000/dialogs/win32struct.py0000664000076500000240000001010510646440134022021 0ustar skipstaff00000000000000# This module provides helper classes for various structs (currently only # OPENFILENAME) that need to be passed to Win32 api functions as strings. # # The base cStruct class is a slightly modified version of an example posted # by Telion to the PythonCE mailing list. # http://mail.python.org/pipermail/pythonce/2002-October/000204.html import struct, array import win32gui class cStruct(object): # cStruct class uses following struct information. # (name, fs, initial_value) # # "name" is used for accessing value # "fs" is format string for that value (see struct module doc) # For lpstr, lpcstr, lpxxx, use 'P'. # "initial_value" default value def __init__(self, sd): self.sd = list(sd) self.nlst = [i[0] for i in sd] self.fs = "".join([i[1] for i in sd]) t = [i[2] for i in sd] self.data = struct.pack(self.fs, *t) def __setattr__(self, name, v): if name in ['sd', 'nlst', 'fs', 'data', 'ptr'] or name not in self.nlst: object.__setattr__(self, name, v) return t = list(struct.unpack(self.fs, self.data)) i = self.nlst.index(name) if i > -1: t[i] = v self.data = struct.pack(self.fs, *t) def __getattr__(self, name): t = struct.unpack(self.fs, self.data) i = self.nlst.index(name) if i > -1: return t[i] else: raise AttributeError def __str__(self): return self.data def dump(self): # use this to see the data t = struct.unpack(self.fs, self.data) ii = 0 for i in self.nlst: print i, "=", t[ii] ii += 1 print "fs =", self.fs return class OPENFILENAME(cStruct): _struct_def = ( \ ('lStructSize', 'L', 0), # size of struct (filled in by __init__) ('hwndOwner', 'P', 0), ('hInstance', 'P', 0), ('lpstrFilter', 'P', 0), # File type filter ('lpstrCustomFilter', 'P', 0), ('nMaxCustFilter', 'L', 0), ('nFilterIndex', 'L', 0), ('lpstrFile', 'P', 0), # Initial filename and filename buffer ('nMaxFile', 'L', 0), # Size of filename string (should be >= 256) ('lpstrFileTitle', 'P', 0), # (optional) base name receiving buffer ('nMaxFileTitle', 'L', 0), # max size of above ('lpstrInitialDir', 'P', 0), # (optional) initial directory ('lpstrTitle', 'P', 0), # Title of dialog ('Flags', 'L', 0), ('nFileOffset', 'H', 0), ('nFileExtension', 'H', 0), ('lpstrDefExt', 'P', 0), # default extension ('lCustData', 'l', 0), ('lpfnHook', 'P', 0), ('lpTemplateName', 'P', 0) ) def __init__(self, max_filename_len): cStruct.__init__(self, self._struct_def) self.lStructSize = struct.calcsize(self.fs) self.fn_buf = array.array("c", '\0'*max_filename_len) self.fn_buf_addr, self.fn_buf_len = self.fn_buf.buffer_info() self.lpstrFile = self.fn_buf_addr self.nMaxFile = self.fn_buf_len def setFilename(self, filename): win32gui.PySetString(self.fn_buf_addr, filename, self.fn_buf_len - 1) def getFilename(self): return win32gui.PyGetString(self.fn_buf_addr) def setTitle(self, title): if isinstance(title, unicode): title = title.encode("mbcs") self.title_buf = array.array("c", title+'\0') self.lpstrTitle = self.title_buf.buffer_info()[0] def setInitialDir(self, initialDir): if isinstance(initialDir, unicode): initialDir = initialDir.encode("mbcs") self.initialDir_buf = array.array("c", initialDir+'\0') self.lpstrInitialDir = self.initialDir_buf.buffer_info()[0] def setFilter(self, fileFilter): if isinstance(fileFilter, unicode): fileFilter = fileFilter.encode("mbcs") fileFilter = fileFilter.replace('|', '\0') + '\0' self.fileFilter_buf = array.array("c", fileFilter+'\0') self.lpstrFilter = self.fileFilter_buf.buffer_info()[0] spambayes-1.1a6/Outlook2000/dialogs/wizard_processors.py0000664000076500000240000003030211116610047023370 0ustar skipstaff00000000000000# Control Processors for our wizard # This module is part of the spambayes project, which is Copyright 2003 # The Python Software Foundation and is covered by the Python Software # Foundation license. import win32gui, win32con, win32api, commctrl from dialogs import ShowDialog, MakePropertyPage import processors import opt_processors import async_processor import timer # An "abstract" wizard class. Not technically abstract - this version # supports sequential stepping through all the pages. It is expected # sub-classes will override "getNextPage" and "atFinish" to provide a # custom navigation path. class WizardButtonProcessor(processors.ButtonProcessor): def __init__(self, window, control_ids, pages, finish_fn): processors.ButtonProcessor.__init__(self, window,control_ids) self.back_btn_id = self.other_ids[0] self.page_ids = pages.split() self.currentPage = None self.currentPageIndex = -1 self.currentPageHwnd = None self.finish_fn = finish_fn self.page_placeholder_id = self.other_ids[1] def Init(self): processors.ButtonProcessor.Init(self) self.back_btn_hwnd = self.GetControl(self.back_btn_id) self.forward_btn_hwnd = self.GetControl() self.forward_captions = win32gui.GetWindowText(self.forward_btn_hwnd).split(",") self.page_placeholder_hwnd = self.GetControl(self.page_placeholder_id) self.page_stack = [] self.switchToPage(0) # brute-force timer to check if we can move forward. self.timer_id = timer.set_timer(800, self.OnCheckForwardTimer) def Done(self): if self.timer_id is not None: timer.kill_timer(self.timer_id) self.timer_id = None return processors.ButtonProcessor.Done(self) def changeControls(self): win32gui.EnableWindow(self.back_btn_hwnd,self.currentPageIndex!=0) if self.canGoNext(): enabled = 1 else: enabled = 0 win32gui.EnableWindow(self.forward_btn_hwnd,enabled) index = 0 if self.atFinish(): index = 1 win32gui.SetWindowText(self.forward_btn_hwnd, self.forward_captions[index]) # No obvious way to communicate the state of what the "Forward" button # should be. brute-force - check a config boolean on a timer. def OnCheckForwardTimer(self, event, time): #print "Timer fired" if self.canGoNext(): enabled = 1 else: enabled = 0 win32gui.EnableWindow(self.forward_btn_hwnd,enabled) def OnClicked(self, id): if id == self.control_id: if self.atFinish(): if not self.currentPage.SaveAllControls(): return #finish win32gui.EnableWindow(self.forward_btn_hwnd, False) win32gui.EnableWindow(self.back_btn_hwnd, False) try: #optional h = GetControl(self.window.manager.dialog_parser.ids["IDCANCEL"]) win32gui.EnableWindow(h, False) except: pass self.finish_fn(self.window.manager, self.window) win32gui.EndDialog(self.window.hwnd, win32con.IDOK) else: #forward if self.canGoNext() and self.currentPage.SaveAllControls(): self.page_stack.append(self.currentPageIndex) nextPage = self.getNextPageIndex() self.switchToPage(nextPage) elif id == self.back_btn_id: #backward assert self.page_stack, "Back should be disabled when no back stack" pageNo = self.page_stack.pop() print "Back button switching to page", pageNo self.switchToPage(pageNo) def switchToPage(self, index): if self.currentPageHwnd is not None: if not self.currentPage.SaveAllControls(): return 1 win32gui.DestroyWindow(self.currentPageHwnd) #template = self.window.manager.dialog_parser.dialogs[self.page_ids[index]] import dlgcore self.currentPage = MakePropertyPage(self.page_placeholder_hwnd, self.window.manager, self.window.config, self.page_ids[index], 3) self.currentPageHwnd = self.currentPage.CreateWindow() self.currentPageIndex = index self.changeControls() return 0 def getNextPageIndex(self): next = self.getNextPage() if type(next)==type(0): return next # must be a dialog ID. for index, pid in enumerate(self.page_ids): if pid == next: return index assert 0, "No page '%s'" % next # methods to be overridden. default implementation is simple sequential def getNextPage(self): return self.currentPageIndex+1 def atFinish(self): return self.currentPageIndex==len(self.page_ids)-1 def canGoNext(self): return True # An implementation with the logic specific to our configuration wizard. class ConfigureWizardProcessor(WizardButtonProcessor): def atFinish(self): index = self.currentPageIndex id = self.page_ids[index] return id.startswith("IDD_WIZARD_FINISHED") def canGoNext(self): # XXX - how to hook this in? We really want this to be dynamic, as # options change - however, hooking WM_COMMAND at the parent doesn't # work how we want due to the property page being in the middle # (and then I gave up) index = self.currentPageIndex id = self.page_ids[index] config = self.window.config ok = True if id == 'IDD_WIZARD_FOLDERS_WATCH': ok = config.filter.watch_folder_ids elif id == 'IDD_WIZARD_FOLDERS_REST': # Check we have folders. ok = (config.wizard.spam_folder_name or config.filter.spam_folder_id) and \ (config.wizard.unsure_folder_name or config.filter.unsure_folder_id) elif id == 'IDD_WIZARD_FOLDERS_TRAIN': ok = config.training.ham_folder_ids and \ config.training.spam_folder_ids elif id == 'IDD_WIZARD_TRAIN': # magically set to False when training finished (and back to True # if a folder ID is changed) ok = not self.window.config.wizard.need_train return ok def getNextPage(self): index = self.currentPageIndex id = self.page_ids[index] config = self.window.config print "GetNextPage with current", index, id if id == 'IDD_WIZARD_WELCOME': # Welcome page if config.wizard.preparation == 0: # unprepared return "IDD_WIZARD_TRAINING_IS_IMPORTANT" elif config.wizard.preparation == 1: # pre-prepared. return "IDD_WIZARD_FOLDERS_TRAIN" elif config.wizard.preparation == 2: # configure manually return "IDD_WIZARD_FINISHED_UNCONFIGURED" else: assert 0, "oops" elif id == 'IDD_WIZARD_TRAINING_IS_IMPORTANT': if config.wizard.will_train_later: # user wants to pre-sort and configure later. return 'IDD_WIZARD_FINISHED_TRAIN_LATER' return 'IDD_WIZARD_FOLDERS_WATCH' elif id == 'IDD_WIZARD_FOLDERS_TRAIN': return 'IDD_WIZARD_TRAIN' elif id == 'IDD_WIZARD_TRAIN': return 'IDD_WIZARD_FOLDERS_WATCH' elif id == 'IDD_WIZARD_FOLDERS_WATCH': return 'IDD_WIZARD_FOLDERS_REST' elif id == 'IDD_WIZARD_FOLDERS_REST': if config.wizard.preparation==1: return 'IDD_WIZARD_FINISHED_TRAINED' else: return 'IDD_WIZARD_FINISHED_UNTRAINED' class WatchFolderIDProcessor(opt_processors.FolderIDProcessor): def __init__(self, window, control_ids, option, option_include_sub = None, use_fqn = True, name_joiner = '\r\n'): opt_processors.FolderIDProcessor.__init__(self, window, control_ids, option, option_include_sub, use_fqn, name_joiner) # For the wizard - folder "name" in an edit box, and ids used by # browse dialog. If ids None, "name" is assumed off the root, and created # if necessary. class EditableFolderIDProcessor(opt_processors.FolderIDProcessor): def __init__(self, window, control_ids, option, option_folder_name, option_override = None, use_fqn = False, name_joiner = "; "): self.button_id = control_ids[1] self.use_fqn = use_fqn self.name_joiner = name_joiner self.in_setting_name = False name_sect_name, name_sub_option_name = option_folder_name.split(".") self.option_folder_name = window.config.get_option(name_sect_name, name_sub_option_name) if option_override: name_sect_name, name_sub_option_name = option_override.split(".") self.option_override = window.config.get_option(name_sect_name, name_sub_option_name) else: self.option_override = None opt_processors.FolderIDProcessor.__init__(self, window, control_ids, option, None, use_fqn, name_joiner) # bit of a hack - if "Spam" is default and we have a training folder # then use that if self.GetOptionValue() is None and self.option_override: override = self.GetOptionValue(self.option_override) if override: # override is a multi-id value, we are single. self.SetOptionValue(override[0]) self.SetOptionValue("", self.option_folder_name) def OnCommand(self, wparam, lparam): code = win32api.HIWORD(wparam) id = win32api.LOWORD(wparam) if id == self.control_id: if code==win32con.EN_CHANGE: if not self.in_setting_name: # reset the folder IDs. self.SetOptionValue(None) # Set the folder name hedit = win32gui.GetDlgItem(self.window.hwnd, id) text = win32gui.GetWindowText(hedit) self.SetOptionValue(text, self.option_folder_name) return opt_processors.FolderIDProcessor.OnCommand(self, wparam, lparam) def UpdateControl_FromValue(self): name_val = self.GetOptionValue(self.option_folder_name) id_val = self.GetOptionValue() self.in_setting_name = True if id_val: self.SetOptionValue("", self.option_folder_name) opt_processors.FolderIDProcessor.UpdateControl_FromValue(self) else: if name_val: win32gui.SetWindowText(self.GetControl(), name_val) self.in_setting_name = False class TrainFolderIDProcessor(opt_processors.FolderIDProcessor): def SetOptionValue(self, value, option = None): self.window.config.wizard.need_train = True return opt_processors.FolderIDProcessor.SetOptionValue(self, value, option) class WizAsyncProcessor(async_processor.AsyncCommandProcessor): def __init__(self, window, control_ids, func, start_text, stop_text, disable_ids): control_ids = [None] + control_ids async_processor.AsyncCommandProcessor.__init__(self, window, control_ids, func, start_text, stop_text, disable_ids) def Init(self): async_processor.AsyncCommandProcessor.Init(self) if self.window.config.wizard.need_train: self.StartProcess() else: self.SetStatusText("Training has already been completed - click Next to move to the next step.") def OnFinished(self, wparam, lparam): wasCancelled = wparam if not wasCancelled: self.window.config.wizard.need_train = False return async_processor.AsyncCommandProcessor.OnFinished(self, wparam, lparam) spambayes-1.1a6/Outlook2000/docs/0000775000076500000240000000000011355064626016557 5ustar skipstaff00000000000000spambayes-1.1a6/Outlook2000/docs/configuration.html0000664000076500000240000002261110646440135022311 0ustar skipstaff00000000000000 SpamBayes Configuration
Logo  

SpamBayes Configuration

Most SpamBayes configuration options are managed via the SpamBayes Manager, available from the SpamBayes toolbar. While this dialog allows you to configure standard options, there are a number of other (typically advanced or experimental) options that can be set manually.

WARNING: Please read the following before going any further:

  • To change these options, you will need to use a text editor to edit configuration files. If something goes wrong, you may lose your existing SpamBayes configuration (this will not effect your training data). Making a copy of any files you edit is recommended.
  • SpamBayes stores all configuration options, including the list of folders, in this file. Do not change anything you don't understand. Some of these lines are extremely long - please ensure they remain as a single line.
  • Before manually changing any configuration files, please ensure you have shut down Outlook - otherwise the configuration changes you have made will be lost if SpamBayes itself automatically saves its configuration.

If you have any further questions, please post them to the SpamBayes mailing list.

Configuration overview

The Outlook plug-in uses two sets of configurations - one contains option values specific to the Outlook plug-in, and the other contains option values that are also used by other SpamBayes applications. It is important that you edit the correct file - if you place option values in the wrong file, they will have no effect.

All options from all configuration files are "merged". This means that any file can set any option. If multiple configuration files specify the same option, the value from the file last loaded is used. When SpamBayes writes its configuration during normal processing, the entire merged set of options is written. The end result of this means that next time SpamBayes is run, the earlier configuration files will have no effect, as the last one loaded (the main config file written by SpamBayes) will have all values already.

Outlook configuration files

The Outlook plug-in looks for configuration files as follows:

  1. A file named default_configuration.ini in the bin directory in the directory you installed SpamBayes into (by default C:\Program Files\SpamBayes). By default, no such configuration file will exist.
  2. A file named default_configuration.ini in a SpamBayes directory in the Windows Application Data directory (e.g. with Windows XP this is \Documents and Settings\{username}\Application Data\SpamBayes). By default, no such configuration file will exist. This is the default SpamBayes data directory.
  3. A file named {outlook-profile-name}.ini in the data directory. Using the name of the Outlook profile means that SpamBayes will work in a multi-profile environment.

General SpamBayes configuration files

The Outlook plug-in looks for configuration files as follows:

  1. A file named default_bayes_customize.ini in the bin directory in the directory you installed SpamBayes into (by default C:\Program Files\SpamBayes).
  2. A file named default_bayes_customize.ini in a SpamBayes directory in the Windows Application Data directory (e.g. with Windows XP this is \Documents and Settings\{username}\Application Data\SpamBayes). By default, no such configuration file will exist.
  3. A file named {outlook-profile-name}_bayes_customize.ini in the data directory. Using the name of the Outlook profile means that SpamBayes will work in a multi-profile environment. By default, no such configuration file will exist.

Editing the configuration files

The configuration files are plain text files, and can be edited in any text editor (such as Notepad). The format for both the Outlook-specific configuration files and the general SpamBayes configuration files is the same; only the available options differ.

A configuration file has a number of sections, and each section contains a number of named values. For example:

[Filter]
enabled:True
save_spam_info:False

Note that section, option, and (usually) values are all case-sensitive. In other words, General is not the same as general.

This assigns the value True to an option named enabled, and the value False to an option named save_spam_info, both in a section named Filter.

If you want to set an option in a section that doesn't exist in your file, just add a line with the section header above the option name. Likewise, if you want to set an option in a file that doesn't exist (e.g. default_configuration.ini), you will have to create the file. Notepad, or any other text editor, can be used to create and edit .ini files.

A configuration file pre-filled with all default values (for Outlook-specific or general SpamBayes) can be used as a starting point if you would find that easier.

Available options

You can see a list of Outlook-specific options, or a list of general SpamBayes options that can be manually set in this release.

Simple example - Outlook

A simple example to move your >data directory to a new location: C:\New Folder.

  1. Close Outlook, so that any changes made will be effective.
  2. Look in the table above and see that the applicable option is called data_directory, it is in the General section.
  3. This option needs to be set before the configuration file in the data directory is loaded (otherwise it is obviously too late), so it can be put in C:\Program Files\SpamBayes\bin\default_configuration.ini or in the default data directory location. This example will use the former.
  4. By default this file will not exist, so use Notepad to create it.
  5. The file only has two lines, like this:
  6. [General]
    data_directory: C:\New Folder

    (If more options were being set, the file would be longer.)

  7. Name the file default_configuration.ini and save it in C:\Program Files\SpamBayes\bin.

  8. Open Outlook.
  9. To check that the new data directory is being used, click the Show Data Folder button on the Advanced tab of the main SpamBayes Manager dialog. This should open the new data directory (C:\New Folder, here).

Simple example - SpamBayes

A simple example to enable the "Use Bigrams" option.

  1. Close Outlook, so that any changes made will be effective.
  2. Look in the table above and see that the applicable option is called use_bigrams, it is in the Classifier section.
  3. This option can be in any configuration file. This example will use the default_bayes_customize.ini file in the data directory.
  4. By default this file will not exist, so use Notepad to create it.
  5. The file only has two lines, like this:
  6. [Classifier]
    use_bigrams: True

    (If more options were being set, the file would be longer.)

  7. Name the file default_bayes_customize.ini and save it in the SpamBayes data directory.

  8. Open Outlook.
  9. This option recommends retraining after enabling it, so do so via the Training tab of the SpamBayes Manager.
spambayes-1.1a6/Outlook2000/docs/images/0000775000076500000240000000000011355064626020024 5ustar skipstaff00000000000000spambayes-1.1a6/Outlook2000/docs/images/field_chooser_after.jpg0000664000076500000240000002073010646440135024511 0ustar skipstaff00000000000000JFIFC   %# , #&')*)-0-(0%()(C   (((((((((((((((((((((((((((((((((((((((((((((((((((4" qW)3]L>ɫrg c'm繟S&H͖Nz|{LP^}In(\"v[SrvKulkmGMܴ7 ^%s>n.Wf>Ʀe_|X`WŁ_|X;tK lcT2|EQkUQkEUTZQkEUTo"jڠ(E,F9cCS-U[T,s|jz%jo ODUmP"#W!薪Rj9D5=U@PX\r7ȆZ KXKUV@)b5q"jڠ(E,F9cCS-U[T,s|jz%jF::KW\޻:@:@BiC,f5=7Ccwn)TsPHM\bL>s mVm9=__Em EBR9pOv*24@"#%1$05NĵLզT*rwY|d'E~p^.K+\J&dJJeZ" OV.6U/18( @HqBj1-]4Jt–8?ƏN㲞;`#.a b /b_e}GM<Ҏ|ơ0&rXHrcI|f4E|fX sD.KE=hFgSgojT8NKn,TKpԈZ4w$UÁ &+\"APemDG;]eJ>ڙkʎLdLdLdLdLdLdLdLdLRTz#T>ڐRӖLݚǿ2KL*}D` a0Q 0F(` a0Q 0F(`1;H 4}go;1QwPb흿G;Avl'u*>N T}go;1QwPb흿G;Avl'u*>N T}go;1QwPb흿G;Avl'AF흿 (#cukF;Lv1ci;Lv1cimid" / / n\wDf8(pPC 8(+O5!/r>j9sP5FuΥu.#xs#Q~*$ْT(K㑫/e?R>1]9j0֒1j=w5sP5sP5sP5sP5zԄw>;ώs|w>?욏LFVHY#+$ed2FVHY#+$ed2FVHY#+$:Q0!2p?m/Ig%v]e/Y/HD"B!aV]ϥ} Q0 2PR?c[YT**!pnm͸y6࿼9]]YYYYYYYt$I$A/tN:'DuI$I$@ 12!"4r@AQs#3BaqR$0b ò?K4FR%ܬ`2DpіLDVPqĴFXYv0#Lt-q"XEְaZ.]ob0X"a&BEFJ$Z%ՁJ;R F 4t0uRpz(e:3:g䰦 z"IO1CCknuIxJP$t: js zsI4DŽMjL;8iipܻ*Zq(YGWXIdJQ;KOQ&11Pqq8ZF-N#f/&f/&f/&f/f/f/f=bfc$zďXR=jGHJP{Qƒ | AK-I3Vn`an8&iJ \IԄ>t'H Ksj5G%NP4֝83GUpZR\KjJUv*':RuBjJfDF-=͹ j*0i[]JΩ$Fp}Bn6Uߌl-uU] 1*Ů{®*q 7z=K^ Qlը+uԲNu6Tq-[ZUQ<Έ*):{IUTWj{6yiDr5TN#IvK[yn֭uj??;t/jӳ(ZD7l 40֍>I5kDQl 넷IĬ~ cOJnqz2GRp R ER X☭gd5XL4f[:jjni)(|EFD^KǣҤJ*GU`{$ܫQӈ_N!}8ӈ_N!}8ӈ_N!}8ӈ_N!}8ӈ_N!tK.GHz%CfdFj!FEЏdb7*7*7*7*7*7*7*7*7*7*7*7*7*7*7*7*7*""BR]Cvc"m *A xd㭸 vu ޒy5ըA`[jUZ5hBN1Jb~j2"~Bu UN%P{0Q\oYR3:$BO!y'^rI/9$y I<$BO!y'^rI/9$y I<$B,wgg2m!֔=(}9̧nSq 7zDs)og9B޷3~!F[e?wL2{Q&sO=(}9̧nSq 7zDs)og9B޷3~!F[e?wL2{Q&sO=(}9̧nSq 7zDs)og9B޷3~!F[e?wKgB{QTdE $Q\>A # 2BZ S!jd-L2BZ S!jd-L2BZ S!jdZc)!1AQaq@ё 0?!\Y$(KC6aEAJYdӔ=h0ET dP2'4 8TB&I ;MiABoH@X fb(L)Ȗ0h018?~JE9ACr/4&VIev `F> 8f"p16;f b*Rj8uHԝ* 䊦* VT+`2B0ZA  }2~S=B}tw܈YƑ|%z;-DC wڤS9głɦAh:x+xvAHv-<4wł$9vX""Eh][<ɐ^ƴFB hZ^Nl!"hA XؑGI]vSz<S`Y8p;f 0x&I8 #@2Y2"S`T <(v 'o"`%=Epj R 1MQ%`3|X#N!(D a c47D P āH0% *}$֑2Z4OhZ!S"+\W?s\W?s\j O$KV#$l!񌱗daaaae~"bse$k[DDp t19v - 9kK*8l(snD4DaXE VSYI0&ˏ]^ą yFHA#B-$"aH6#AJ1 , " 27 (e$cA(MPf܀l_ZEK ,X$I`%J6t+cwUV;uXc, LY=vֵkZֵkZֵkU ~zvdH|M5]7<8`AA 0 1<<<<<<<<<<<<<<<<<<<<<<qq<<>S Oݳ0a! 1?<(RiNSv'onrG4rG4sG1Ê!B'J/JҲ+/JҲ+/JҲ+0!B[itR)JR"01a! AQ?vD}P~&TJ:WP}{f^ݙ36flّ+fv'@$!| @IU,)h,)h,(RH$AA Gd B!D"B!q)!1AQaq@0 ?,T7&\skbD3 ⶳ[eIb^+e`e"Y¤KrV߆L6og]ukf?CuߧMVr1IwVkE]u5P֣qPOxFoD3N#O@i "!EfSD&i*,PoHW+W"Y2VfvG3(O.4AF Ar*@*Ņ .F$GT;i-7D0ǀ$!U59$ e@Xj8ˇ8pÇ8pÅ[NIМ`ޞk{mqU0FBb1h5Qr:EG}m4 cb4Qw{a#>E Xێ1{W T\w;[q"Ɔ,m=.GThcƑAlcC6FU#4]l1|GcH1q#Oj᪋.w>#n1P[ōpEs q(-hbqڸjuF8یi41cn8i\5Qr:EG}m4 cb4Qw{a#>E Xێ1{W T\w;[q"Ɔ,m=.GThcƑAlcC6FU#4]l1|GcH1q#Oj᪋.w>#n1P[ōpEs q(-hbqڸjuF8یi41cn8i\5Qr:EG}m4 cb4Qw{a#>E Xێ1{W T\w;[q"Ɔ,m=.GThcƑAlcC6FU#4]l1|GcH1q#Oj᪋.w>#n1P[ōpBIcnQ(m~ 1Eopu}»B@HyB;d2n ؀=]5" (3%6orI$I$I$I$H3$I&6$ H0PL ? 0`RB/:&rF÷"%^m^m[zn nm^m^m^m^m^mCh,;+Z J40B Yf B3%3x]qw~Xy6ґRBbBB酋Z$U*K5=: ꤧV S K tE%ZL<@$:C]iLȿZA -` $XOa]߿~dh 1UXDhRO:tӧN:ZLat+&-spambayes-1.1a6/Outlook2000/docs/images/field_chooser_new_field.jpg0000664000076500000240000002271210646440135025346 0ustar skipstaff00000000000000JFIFC   %# , #&')*)-0-(0%()(C   ((((((((((((((((((((((((((((((((((((((((((((((((((( " ޑW%݋yfᯢ:szisvGE.zgA4IܓLs%Ub3!n+#]-sMtiϥEקKM8 wp)N>])\%S&Li^JdY*CFYdrZ7\v5XYZ7ܱ†Ʋ2222222222##B>#)ڣ5=yU&@'n())h}\d7+qatJ)$b]VXWbbk X`޻+\!uC{X:Ӓ!iuk VFyftkk0O<=<0O<=<0O<=<0O<=<pp?,4 1#$5!"2@0AP59YwF6's`,o9Nvg1Lv3a))!!!!)K̇9rsZ#!eu5jwqL*Iu='kf,nOmC*C#gtnZ%tNuLU1ul])u2Rr1^cicz:uuuuuuuuuuuuuuuuuu:[ŕMKcGe^ Kё݂KbN}%m'+2uV>,G՗ Tt_U JtslũAJϭJz/?1ṡ2s!̇2s!̇2s!̇2s!̇2s!̇2s!,sE:MMMMMMMMMMMMMMMMKG8Zdi%KL-2ZA3'sOc,<ㅃ3332̾e/t˦]2L$B!B͌c1c-B!B!B !3A124a"Qq #0BRs@bPrc?d8LZXjl@ȶ d[2 'P Z1tv9VbU*؅lʶae[0YV#[pIj$7d({b -Yʅ7GZL.kÇ C5!ՌDf:thGE&jQx|X.suY`B&ږNv!E4Y[EX|Sؐ0:Kn3s>Kn3s>Kn2s+j2+j5ڇmC涘kbխ[VmZڵ, i?H8<ǒc#Z!Uo1%HVP['+wEnޓ|"zOVI>['+wEnޓ|"zOVI>['+wEnޓ|"zOVI>['+wE4ȉu݋"3H9NfHb3<;S?3R#m'ޞmadnP‡0hNuoNC^j0W|Fֽä_T ɷt\4s]F]X s]!ıcCbn6:\J"d.Qq09μ:gV}gLe@$*Ϥ<ØvVcVBe"_Zf:ζ}J65%h4`?(#x-mkhs[C涇59mkhs[C涇59mkhs[Czn=\U̯e|3+_ fW2̯e|3+_ fW2̯e|3+_ fW2̯e9ϊػ.35^iZuN>XA [:h#F5A [nrA [nrA [nrA [nrA [N,*3:Q tW#%F0v.85dbIkD.o?EY(tZ'W׆-x.hOuRxetzUsbiYi.m:jpw4ľ)ugCAG-Ub>BEuV|F+Cc:&F0v.DZbXV+bXV+bXV+/{bO+b'S&H(݋" hę*О׍SiͲEsL zKzKzH@hg跪Na~H-a䷪Na䷪Na䷪Na䷪Na䷪Na䷪Na䷪Na䷪Na䷪Na䷪Na䷪Na䷪Na䷪Na䷪Na䷪Na䷪Na䷪Na䷪Na䷪ORlQX QpUY9PXmn-Ѵ6@VѐC@f\di=Z7F:^u;嵮v_v*L]0Z*4j^*.)u#:ɐ5At*!ZLIÈl1{eocuu] nP'7Y:!=3I%)3F9hҗe9ppķR) 㤃 k`v(‹Qbe@ku, 9RNÄmTsLVV`mA4=5++Nt!CVFRh;}#شՊêjՓIZG'9?/֠t{#v}Dn=)j>?D$8HI+=8phVzp9!&ЬrBMYÀ䄛BӇ 6gVLUv .?x>bh٣+f>bh٣+fC"~7bqk*!1AQaq 0@P?!,m!{n :R}fZUdko~}i$EDFš|¡|N6^q/tzL:&62O0M Yw_if=GSΌٹeJ{jhi)-fj`Pa|xB!B!Bޡb]S4.NbK&VX4Uy͛UjF@ Cxٟ6gv;C^״>A{C7>m{C| ̃>qbcɆ/ :t{0~v]]YeYeYeYeYeYe5Z{0Ncq +-\YjD<}3gϹs>}3gϹs>}3gϹs>}3gϹs> &x  0/[Alz_<}5ώsõ00 0 0qqq?qqqӯ8<<888㎳<o?{}a1A !@0?\}^ .kv]cu>*7ꪪ$! 1A0@qa?IͥG1]$%I:h%l#Oo$EsbzR   ސ!B! w&\ bb!C&b?)!1aAQ0q @P?\~//b|h s~}/G4Y"~`Z*摭rBmsX9I력K(VFoR[;A/Q03$fP\T~zE¶ro'OgA)u`&Q7j6$PZ2J)bWOJ;- (L XYp;B-(70]3ьxs E L Qcs†pg(9D@ <~G|@y~W_"tuj يoN+j n<28R@h?|'OO=?zSOO=?zSOO=?zSOOۧPWAƚi>V^):ܴ#1WUAVfUUJak$$^c:q%k2I-@6!N^PQ!I8K"UJ !@i_KsQ7Z+\M(LA=7{1* \t#A7@** ipR-CŠ'] 8бZqiŧZqiŧZqiŧZqiŧZqiŧZqh  M[SkYǼ=q3y{g8Ǽ=q3y{g8Ǽ=q3y{g8Ǽ=^nKsUQ Z1ִP[S Lݹiq /=۷nݻv۷nݻv۷.aPѳ =iCFZgKsUq P1{ YDo.wݘ/8-(6-1Z^h=XeT`Ө4`,@U}eB|ϟ>|ϟ@= ajh6R3PZVZz[0*Ү4,l&IkǠ̘\4#ҍ( )tP( v[Ʋ,lUjT&4m\4-U%XkC|FS1\^; 4EJHZOlأWC\]:+jT˻i:XuPK(%Ȓ!< RХ#] sUtt|uS) O3T @rb`[B6$6Y ܸRЛ1I>J.Ksz[UK˖]EK A(.?4jNVgh%9NNU#Kte-c ":,zy5D04iYbɖ#z[ #Шi,n~Ks `BBսȪzڃ75ʷ5ʷ5ʷ5ʷ5ʷ5ʷ5ʙ]a ?l bV5? (PB}-A^|~{{{{ spambayes-1.1a6/Outlook2000/docs/images/manager-select.jpg0000664000076500000240000003761210646440135023421 0ustar skipstaff00000000000000JFIFC     C   c" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?k}Zn$ݘF c02qFk׾$xRm.xat3Z֤'ޟMN[?=ŲH/.;Ye6"*d]\]$W3CۙꍩST=G_>$|)Ү][u;'Ntt "uDʱ6ŝc?K>.<(ڄeƳCf'*ݷzអ?]^x[ *MYҠhiwp^HǖA,~c⍽DYj?"l5mBA}ڴIvHUŬYrqzjF#U O6ᥓז;դ1TNIK>]es:o4oZyu`ylKy eEC A?Ÿt6X|)i=dž4H>s6},Vo+,=P2Y,m$ūj/mV -WĞѻNw[vUVXhBA#rH:+_kx״z ǎΖwZ͓XiCq][%ᶶuqqA4sd))j@gƚ74Kmgņ5 2.-lJYNQoh .um5;s]9 fU<S_5> OoN| aqk-K6iŇM!ZC o`Sk on-OMKH6rE"BKq M4nvD<3Mƚ%b^wyrbD%[ +Suk~!$~:oCy|]aw[INyV8HhuxlNlѴ3[;x߰|gm=dx,%U9IE ^{G:5ψ3D+ז=nNS-i R$%NWxkH7,%+Bsh_۝>o w6*.2q q}T5ׯtoAG4K&o I;Ag=PL;>x^pff?~#£S?Vh_?*5?H|?:~_AK x{OVjP^r?/o_#!5#ʱj |D7M1mŎyYm+R4V[ĻLI <ƺދsEI¬пTjG* ~GF?hzŚZo/G??fyf,+_M;K&u{m[ƲHFbW˄ϘZ hHrjMw;1+HդrYn$W3St;!mF S"?jƽM4 i Z8uH M+5 ڊ 1 =& I ]sĚ5kQMGfn"@ cdU!xE%Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@j\"ۤRJӲ(* @d5^<[={KMFL "%0YaC6c*NU漻 пo=G6' Ե{{IbxvgF܀\<7;kWN _zmE1noy{ Q51<$T)$<{,j>:k96xWĞ9𷅵yehWSdx~yDBjUxw&j>ĉ[ETK,>suK.KaK<}C??<}C?<o_N*ºơ;4vdq1߅Yce}ňVKti8%7jZaJ fMʿm𿑫Nj>v{efmi2ɷZ*>y?}å˦xR=WUHk|먯FYckԸ)'}ⴟR5 x6­xvi^%Mk6J018X<w|a_LEukH>o=w,ܒ֨Fb+>tm:LG:&7v4o՘\+ϋhh?5Fȍcn> \.SjZvkSjy,mŨ*f?2Y8nyJ~<OntG =b'x RRKi%9X4CR~;|YO߂tgmAb46H#ŝe¢ ?g+^)ug,䵳mY?Q г??uo,+(mC :JCgPG6BΡҾ?>Y?Q г??u+⥯#&gc6XpJ8:Z_N~'RD&4~^5u5{Lhxnlj~8x~ _K <_/[D6ugT8vϱe__pi}vykvlwq"yB#^* w$zCtTk/JhcH(&UQb3&Q˨{-O7.#)Z]_Y٥*d &6du]Mo<-u(4΍GlZiŒcۥ[Y>pHۗo+r= m}w_џA%?m @,dH̑==T{H8b|-~1J M{XVF;n#"H&I>PZWZtAc[9kɞ8WdhN?<-xE--m(FТ(F9 V8GkBo\^iqj>kolbn> BOAʉ&.ڗ ]νhuc躦X\Y]5Ey/tC<@ %KB ~?fҵ Z}6Q \]E"0Zt1c!D}Xg;[K|k}?ƲMq-{_^$rBhSO\n>ʧv^S!c{_ jvڭ&/,舯5ʛd@rA PD~kx??iqWlk/ q)e>seF\7-E߈-ծi.+]k=i>ѫ ~pj;_jK*h=v٠{VĄK ʪrsY_>/U{; Y1Khe˳H@9mixº,S[OtOX5ڄ0inR3-հ2A 1GZ Oh:Ni֏oǹԼ+ KҘdHdRY+xmҾgxCO[;Rk;X-`a-UXCID xmCĞ(ٗS5Hm!Ym"mIrB+9TP> ">x?U1x6g#GehMh($#[M~pY~z-gS𽖯yCm;Y,d[;Ka9)t~Sy=jG?_ xo側v[ʷͥm* {j;m6kiVil}QC˳1bķ h >)'_ kxr fDc}R--H]̎ X$d. ~a? /cD|^MfZ|4ˉy GEB۟nhRb[Oxڵ6noP;[AxpI,9vSj)O/n|c?h iLښE]:[q`Tm^_N\-wYSDv,ᯮ*=q2HX11?jxQsyi>]i~og=yM8gV*캑~o^>hv`m%{$2ż6il J: 贽[OK]Ե'R鸘\i˧ Xǐ1yNXm@]aCC/vdkd2Mhq&#|ft/R?Q[Jt-o!.HK;l_Q{G'^-'fm/Jֻ^š>kK:Zۺ ,Qǟ͐]ުxoΡ{k3kW_h+ˡHrc6[`C8-qĚ\aָu;=&At.fD앜Zb9.Ba t { ng+xoWmKKٵO۠="Atefd*G,%!SQ}[T/Ԥ6y{{?hE6Msa*zr]&W}CV𾇣Mh0-k-a%>ea%é/jψ +iz}qtq:?uezeGs$"LssX4 [j6:oqo}se#5n{euLF>LfZ??3S}+V/4?gdk}.HSSM0cc*f xvյk#Q·qosI~Sh.-F ,@ERQE]?(YnI dQEQEQEQEQEQERs?–%ZZ_N~'RD&4~^5u5{LhxnljkGˋ[k B}GOIM1z>KI 9rZ3|35MgO[::g%,6pʷ_cO!4@h,ƋQf =/" ]KP r!a7kTa]#:C ( ( ( ( ( ( ( (?6k8ۧCE??I!*| lr~ (r~ *| l>*| lr~ (r~ *| l>*| lr~ (r~ *| l>*| l\+&xƾj:m.4R%w1۶89'}A^~X_<'wn9j^]jzQݻ /1f(dc ]5l?C6~?ͪ߫O1@!,E4ڒW5G^ee f$g~6UσEHf)o;X3x%%$IĎ~mWZ?UVb<_:<;sYW峽o8u\YA?K)D,wgׁ|-gkzw-uHypQ&/~F5~"x#W_ý3ĶV!uQwg_ZAYgocOm$\n&o؟czl;gң!O%aO8v}Sú$w:^EN'czlnD\U;>F2w<% "Bny|?ؾ7_MĚ/[{NDXZKcp["biž5iZ#~n OðsjPj3l 0%, DFM&j}ETQEQEw/+KB.i:- Kަ5.d#CugW|}_xn53To~&5)-jE܅l(aEG՝;:+%jiݬ̥RU ]g&ik}gx@,lu3\4쇉٪A%QɧG'[)><6'GOWJdGCچj4;R7XrHdR!YA@'(|"}6O^̶l-m6ed@>dHJ>O _,('?+OHs<>Pyw$ך5ճȗ[M q"g%"wm$?"xW $ۢOxW $>O _,+n'?+OH$?" OxW $>O _,+n'?+OH$?" OxW $>O _,+n?)[b}tDcC01J*NşVAoEPEPEPEPEPEP^~X#θ|Xq(Mw4_ FoQLG3Wt]NO&8os&m:=W\yW/ΈG~˚VǢ/ JuqxZ jh4ȷ$r΍QHg_ƥᧂC-*hȑ`L6udNUD VR6^/Ӭ^pjj8H _-Q>) (((((((((('eYMfV1kmݓwe. T)e Ǿ+^mꐉ/" e`Ueee` e|bGux;?Mk-e+qWhRk6SV:]Ym)mkgi "H ~rp,O$?k8ۧCEYQ@Q@Q@Q@Q@Q@|O|y |cCnƩY1H|ͻvHXzQ@P7d'')l."*l$k ᨌgn=\@ -?> _ q@ -?> _ q@ -?> _ q@ -?> _ q@ -?> _ q@ -?> _ q@ -?> _ q@ -?> _ q@ -?> _ q@ -?> _ q@ -?> _ q@ -?> _ q@ -?> _ q@ -=>'xP6 Gf cD<($spambayes-1.1a6/Outlook2000/docs/images/manager.jpg0000664000076500000240000013776210646440135022153 0ustar skipstaff00000000000000JFIF``C     C   `q" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?l?_#^<5uh6ȭ+NI iE_O ׼c7"şlf,<ƍ4QsM3Q?W6[cHlhts"<.2Jx8ѧ&Z~EYVs$`Rߊ 0NR0ࡿz.%9㟍2x:gHKNJYndK{[#yV)h}Zao⿆nUy BmvUDݺVy9k\r<`> Cڕ`^ Cٿz6拮NJGv8Ziw9pPg! |. :}|>Ч7ZfXb%e{ca?+R_}Ӷ޽n;9SS"7|#*|+i3Ѵ\[X)-on&a)m.9+W]n4vuN{O' /9S/ ?.eGڶyf}?ct|~to u2~=n?U͑vqvhZpxWgSx_|jjO,ϴ/ctpt +?+ȻUT>о_Oƭ0 U:~3ooGK7 >}nnj[a~`t^ ^[; >}n3s7,_cu|?8ոRyv}g'=h-_Ku׍Eڭڧ; >о_ H WjtJl4?'בڭEڧ? >_ BxDrTM.:/ >f>Ԟ_h>_ ErT_nZ7 Ҽ.j.Կ ug:>1l9_WڭTC _[[M'L]+# VS 7 gr5fm/# >%j.gEVoBgd6}7"jt _Mr1ը}G >_'Xi[ַ[\u .--̹;b#G[&EjkҵM.{' LV# 34h)ӂp5jW9;[|A_":?~ɳ?;'~[㡩|qiqi]Khʾk$ŹTFNab7pF@9ٟ:5ǎq -FwR<ϴ>dchNϼRҩ;DnpG??juT6W#ԝ7A)=_&zn#] x֗GgXfh1rǕ `n\_jhf7-<ϴ~;?u^w%/)lvLu %'EV߶w4hggmYXjS̒{Dn$^ Y*5F6[]vX:SߙޟN߈1 ]#\=}xþ(u]?F^Iml/#[92˲eV&68`ocyO1 UW\kG9+VN_4Bn7Df $9`w_pyZÞlDOkSп"o3Sho5_Z]jV3suײ-@<B $r>Nc/+M* 8ZXY.$Ic'3E۸u>RO#R?jO?1ƫOĚFummjV7X!H\#`b ~x#zKw}CϕUbCF?e/4/`&sKêYKſ̱K3Ņ(n-O?}sKs}CϙbJu2~>'_nOeu+,4Q$nȄёPAguu>xOٿī/W;xz_O7_DQKo}CϟS|nO~ ^g׹VT =p@\HL:OguEK0$G/:gw?>kNՋ?xwǚtu/V`{&;@-SaKk}C<> k/Z"_Շ"#B|Q~z/^G _Q[ZO4{gfe'.]nfIryo684]?O ]/?SWWAE/|Wundg_?\牾%Ki0E-2x"%c`,IfbK1<LƵeRzCJ.o)^2?|"|e+coq-̒KMs#xY0(GX,\iEklܞg—տxzd 4_꯭z4sϪK(Hz7\굋趡bQJm@ o((??)YoRn>R IO]|MouCkokY+#+2LpV-O 柣C V_iuܭv0\Eg%Qv['ѴE;hZR[+}l2!G]C AѪ  O/|efڀFWvz{[$xfE H>[_ gP*R²x*&[]W_HJʊr(%-_6t+\FpG7X}g};N,h]'HmڢCVfѧR䨕.UPcay;?UՠҭR-/A׵X[i?.%_co2۳c%J-WNHnbx]x% RHʺ6 0<V ^ok87Gvf.;-$9,KIu,x7>(xmSmO|bK3ǪڥD4Bx՚k!%uz;㿉uk_5? Xk7Ew$7ZL41<䨖)k>J>7.:VG!yL2˜y)f9˫:OmOo{{ݡY\H2R+B$mZw 1j{~)cxnN6?F 92@l2]jIV?1L\VԼW"z˨,vG@c!Gw>uWZºTU奵./NћGO}B)YxTs9UI7Zoiiw"˞/>$wDYw$;|<ǟھ/Ҡ\,O#Hq,g|Y5?V_C'<'.Tmg7ηe 1Phc5}/Ewվ!µk(,a[Kr y[C~o/wX|-y?2ۆ[c͔RyrMwn3j|Nh=[;۩/f~sȄM @;]ݎ~.?gi(J<=ĚZH\q5s5FҠyLoOV>?^-@<@d(G#dF=N]'&j6l \K< .̠}CH!@ƊcC OQxFY,-im呙3"['?N7 "ѫü1_4ekv<:snnk y-'NDay7\˺!37RL,|VCck⯵IZ6t{,--LdH8?hʼw",Ʒ6? ͕^ԧմs0rFwb"9mE;cU/+׾#-.( dYBي_;˟__ ;JV%Jg{˩nn$ .gIX*HDEUP ~.x-A:^.,6_k{m$VHM7o#Ek+o3NJ K_?fӍڵ-75ӆ*$"nRUK繵k)LKfBt$|RI#kM /]GS{—%Dֱ^Ag,zSTf]BZI$\x> e;=>Ů-{ű#(ϯ Ŀx~DE,4LJ5mcw:8HcXc$]Gg@|<>ץxIl$TA>:!2\yUg=nTZ.$vo*cRv\jXѢ) ((+%m[U趦f(Ü??)YoR_ȗ>Eq7[UZ/|[|\%uJm`T]YKȷ6jakF޾n8*c:ihZ=y9-t"b0P0㎼!N⯈L@l5LӢX%HXcHr `dGkAO<ҴD@WQ\Fnw m3ñ:[~"iGG!F#9ʹc$S />?z 6Wf\h$-lݡ71*UT[? CjM6 SFү rkԵ^䌭зbvA<;/Y_˨6 zr$k1!J0 E2YcgG0OhC~o>=9Voů1W|Uj6xK^]w$eoݖ;o k|Y߆,I|?^"., 6u =[ڂƭ ̭" RYd>a-n=5-U?_?[_7GB߇Kk5-U?_?[_7GB߇Kk5-U?_?[_7GB߇Kk5-U?_?[_7GB߇Kk5-U?_?[_7GB߇Kk5-U?_?[_7GB߇Kk5-U?_?[_7GB߇Kk5-U?_?[_7GB߇Kk5-U?_?[_7GB߇Kk5-U?_?[_7GB߇Kk5-U?_?[_7GB߇Kk5-U?_?[_7GB߇Kk5-U?_?[_7GB߇Kk5-U?_?[_7GB߇Kk5-U?_?[_7GB߇Kk5-U?_?[_7GB߇Kk5-U?_?[_7GB߇Kk5-/?Kqڮ0Ameomoi 2m5BQ<趧k0~LE`~k{[蔮W%m]V׵J|q"^`ߡxC|C>mIXj:mǴat]b6q?Կ_ }'-:ok_~O#| G[HVH"$Pv)}܆lm7߾oxڝ[iգSg$.MLNH(LKS 5}4,ghKVM䒠'B7CS?;ԼeꚭԚwمΩsKMcKt/&zi"(X;0SGpHt?x{Ut: Z0GxG QCۅu;?v]WABim$^tsJCa#x] xիAضggkk4kmGs,?ysu7gc[( Hf{Eoc]>a$6uAv{<;x3Y|EvNb9#+=bATaݴ;V>$5=cSlK+WKc"|C.4/OBh~ɾmUXnd.uMˣ|/ou /\ѢuVĨtBk8SV$FƆ?/?7!uo7:oK،#K?[uԡTx<ؾgEq:S5WDaU Mn6[^]Nmb V;dU>SH.q+McVgNZc& elLGWB]e9Uk^`igWOVi-HEnrH*}A ^kw]KTA}eu Vė wZmK5>OC:1wX+Y[Zσ#z|z[[ $6RˆwLn!G]-; =]>2wWΩ\֥^Ic+YǸ3bWfAPFׁ-ykozvR.uH @X#~u~xyhɚNQHm[, 2k >a/lZ|5jvcye6źs\-J766A4r 舋 Hqk[0UB(<z4zF<-\^n! ]zih{t\ɐ}ӳ;APjKt௦i?7MlY5[tPT-{FxX,ڵGw._So3*?5 _f)Bm|8߈׺miqxL5; -F9!KQ307p<<7OJO.k_=wY)$WfUb1dyž6 ZmGF=NX_4DաcؚrDϓP0_-č3Z.]X Ѭu u8^Cr2ɷW~Qڢqhjv ~)|'vw6QjUAzϦg% 2vU@/Y?Jt_Jۿ7oW/\_zv{;<эY.iK6<ľ"/^$=Ɵ[ Z걜E=VsjJaTUg1L.}kk5=q)D:Z gجLH?:_ ^gtOFU.1R>fрP7895~9om ֞$ӥZG%ګj6n}Fޓ9|ͧqcj9otEobҤ TΙen敌 Oyg;=:=C %%֭yM!,B\)Oxv-~ -5ּ\$q$"ǻqeX XSI&U.H/4:FO4yvU1CDÿ٫Wù4N6;JJY#g1?=ٍH; aúX| j^y F9Ӥ&h%X`>|ek[J4Ij΅אk{%tW j |/oEuhQ;·>&P'E)bIA,2s~~ݢrJi } ,8Gdkk5=q)D:Z gجLH?:>|A!<@l$/涹\Gmy5̒( @2|_@m#eI񎧬-Eo(R9,0aq|/GmeZƫjsE\LQB ,!Fe@"EFUUo/m5ˣy>vM! chmD#cb5=S~]}6HMRDo ȷ %aø,4{ eps+Kesz׉MeoecGk!Y j2F4x;߃}ky`6Pfl& 8 ܃:rysrƭ{ھׂ֗}#UIIo*2G+I$JBI(YZOu*^VQ+UV*m<?3F3WW>)[ 2{wKXw̟P|JF }q};Fl֚4ͦf hy{v.ѕ\+Fqd7t}Km#EҎ(=wPp^\+++dt|IԼmOW𖩦Vah ,k"c{p%AFL֫{x< !)kSP+< *1kA41I 򡍆vo8iڇfK#oK;;Ϙc6ʪ&:堟&5UKw7HҪE"V]BVP59߈Wb5?FРӵ ;Zm-@qZۑp&I%MuڶL/M -`G~ޢr\|Zk,jJ bc돃~ծ%7K243jF#f4}WgwҧT+'n~Ɔ;!<&e4*Htdr51|=W񥿇 i6};U{yԕT=[6s)bS(=" ]f[K/.MͽĖnĆV,̓SW|<'e.jQAwoE1Kv[yqW>x+C}-tF=ϙ#}Yfϼ#DZjKN@lkۏDg# 4F9t>i~|5+,eFeI60$fu{&/ݧ_xZ|ou$iMlY!mSx{~"k6ծ,z=&kk#tsK2Cyg2#qb4)gW[m5OYOڎvw\l%hd#2B;ZoxLt$ݜfxXn-Ӯ!e*61]~ѿxT ZꚆ}͵ݱבSH´qQX| &6xv 6tjSc ü194/>Yv{计RrkttfYY(. ;+3HYo?O͇,,XFLdPY5lcҵO kkVwȞC<6HF lL9K]|}a%7myG3+חH)!)RE!dSLVv>xN[^ znbvKe(fiX@`h5=(QEQEQEQEQESFmg2Pz?[U= VUC%mU l~LE`~k{[蔮W%m]V׵J|q"^`ߡxC|ϭR3_6->x |KI=4Vǣ;MFOehkYЪ\L яyg+ EQ^/ak]ƚυ4 \xn[u\.?Jsf@ª%;k>&>3д;<YjFn&үnHⷜo%ك^+lr+/4 E|C1ZV76>&_j.\þ󤾒Bb_S[e;+X5=">Y +Y &NwfVnUvwIf=~|z-Cl4VIir$IY }4c0d>~їz=-д p܉5+M=0^vKgEi]4,TWϿ D񮹧i{"/Gmar^=ψ$Xc]g/-ɑj#kuMK>zvt6QkOZO$jv񪼥K2as()CPq _f :mNUnHheWhyIXԱx%I򭥵t^^=i FeOkm\@m2d,=ZkWW/n|!enމb֝N Y ',4P(|lt?":t{Z֡}gmuK32巎#y-^9,T'Yh?x[BտWVW0 cXQ&aERBF6wj+xUtY7i)u){K9!-%vZǶ;HYyE\ +OEރx&jPO-?MNw.ڻz,KSKZ,$n%I5Xm_mΙ?.- _hZ!Ey2ҵcX㸕<@ :)u AkBf],G܉mdYU`Eq7u^!_2f/~}^Y[7:޾o6e(`pD\8$>!_2f0GC=|?&--cn[#]\ZW<5/~ ?PovϩZk r9fVz ;q߁ l.J,+ 3Qi-Lf䴆DVɅT1 KQ@4g Z{jfim/=;A2,7V@`w+C1`K4\\^ivR%յ7JEV4am@X%5:vܚ5ߏ W|9BbdS5l!ad\-i v >@&K(KxVwmkqsnM=R3H(p#EUE7xthZ~}(]i37R-s9olmF3`ۛ>cw|oPX~O\kٺz5m]+&]*4g@Ѿ){X|IcCf,4mE%d XЖv"ggkvM! chmD#cb5y|OsiZvmxὊK+k6u[A5.|UH0?π08r#͑CdrP6{\il_d.fN :"Y;xd<gO&x º|6M'QH;yeI~i 6ûG=><ұK[)LLy;Uoxkۦ_.$[Žʂ=أwfH4nP6|u-o4f澸3Fkp Xy"t|Cmq(.oI4=bgHfUK*-P:u]Ent{Sv1C͵m A#Ea<PbDFw|ZK$ږ:ķ0YbJEg5 ҧK)lLnlKSy-! aU.vP<(߅5/=@-Rl{q%[OizzPvaCѾ:Mi(,l$&4s6l/*/`XY-u[}Ɩ^SuTx?,.$ex򘦩*n;Ac ^|4׼=ߋm|Swڅ։< ׫ DH oٶċ\I / ߁< 㛋OsmjVI9HIh m̬-X.<^>4}rUij#mwP䶽6KCePڇ{,!{ j·w.gOgsc۱{RnϗSj_#񆉬4Ng=."XyyIfk!p76#QnmX_ ] }}~?K]}.LT`bUb ?V~= {^Y`AѦ4X4ۨP4fzz\*͗F嗣m^ҵkvW[&woJXB䍊ɂfըc}|ak6"ӬY/KȖ1^^FRiY\ ͫH6f[i, >&i~%OѮ\o1kp15J*0f 9|/u >ƃA~-tiOԢ.v jeZhce1o ]^^ѦMܳI5]٦EkE_ꚧ|wMJPWTH.ͱdܮ$r,^E /'5)uizkj=$v}-MF2;%V;Q__C&Z]hlDڌ 4c[+inmX.<^>L$i)"[_hr$G-Zye+]*W)i"E5"7>.ͶPү 崉=dh{^gO/nH(X ~M ߋPWӴ;xoʳoIK,Ҵq"})[P񥗉a'}V lZ {O_}ͽUSjBnz\,x}|5\ώ'եiS\޵$icX&Whvq[}@tkvj?`d|L\o_Kz\,x<#6<9/>waȾWq.lQd|}XƖvͯص4 |vd& j <끔 (X}4?4+^OKgOgپH&1+KtRKoQHaEPEPEPEPM= VUC%mWo3'YUȗ>EWT%3EWY!oR_ȗ>EuZ~^%+z?[WS~U_/k_=@i6֡sX{mBU)%[I>%M0O^!_2ftK'kD2EV@w |8''j(0xɵ֮m/4[aM Ú4]m}4IdndBmXbx?|bΏKIԤ9/M6g<sEtn`&o. >J;+𶊷6VX4Cm4ە9$D Ȭ /KS.o<-wRXM> ̒M+Fu,7O4[ߎt{M{Ts,bFӤD,/H3\ #a8@`axٝ㷊#ƉO:fmlrjzm첲N/Zgd0>ܲE27|;sMir4WpKUq%0+<l _Y W?w+~NwQtgi^:m{KxcIזkyYn\# Ͳ+d?85^ Z ֍aexTiVqs^__&y#XeRR+|etEѯ|!eu4I+n#)r‹d" |7EcuMz&{5վ.\k`G4o}_[Iﶣ1xgÿ|y'vckW~׈mcCaҼ j}:9$iU$L2iT2o^.em'U>J-coooe5.1#Hm´1L dV#mvZ72 -k?<+cu/M)JJ;a[o&F$Dx|7EcuMz&{5վ.\k`G5b>th]gE@"[w@Q xt; R-.-"d>"Ei+iWZ5Gi5/.MBۤ՗o1b}'zF3KӼXlShwy0b8ڝ{` ~KÞ" h^}T{2UPϹcr@'W@xv:=91xWtSe_Kf$ X00˼lѰYS]\KRƗm[i%V7rK"YHY:L7|;sMir4WpKUq%0+<l-|x@I{X50ye8$};<-팗W>SȻky-b!f[A4g{KĒ(vIhs}I歫Cn?[KKg>xw$>l^w "oV>m+H[:=>Yf@:rsɪڗ _k^$BMƞ(m"DH,vix5<~&'x7|Y^fg\h3[y^|zﷸUfiX-] WQ=Z懥k4.u r r6#HU%PI/ٗ5-z\PQl"xfxQၶ;1%X@#촕J>b-AlLm_- ` `PC_ƿˬMy7V,m,YD eZ$D u˽IIS{ح줰DD]C O*$E aiַ\gvBFD\ " ћ>h&4a+%*8fp@xvM;8|EoG-2 ٵ> . HƫYha](P\Ci36|Kߊ+Yh+Y LYK q)"v29EtfQHaEPEPEPEPEPM= VUC%mWo3'YUȗ>EWT%3EWY!oR_ȗ>EuZ~^%+z?[WS~U_/k>;>RZ.CZEu6 ?5jřHK#06|_D֣f9}Q&YJE53+ d7S էl7ShyGwwScU•+ >0YƗYiV65 p}<E+:EY^)Y2K++ir^Ek^ϊQdͯՅҳI$7!4{HMnbQ|OO.T/|Ez+CõҲ"$q$1[UFz$\jۻmSUVG+I%ʨqbx`CѠj,X5^eS-[,O,KZnys3FO~~h<%k8ح{:=3 yeZ4{e?V~ ^$|Akڮk ٵ8I-LDM4G/\H;-[uFug+ |XӅkf@%8|e S~0j|z^I}^kZPmLY.mqĊ|ò>6\xz=߇u)t~D &$\7DiVHH!  ɕ_vV^05|A&jzݧjlc'qs^\1Pp|u}VT5MT!⻰hYoլ5) eux¢"I jf  wﮎcK* X.2~r1+z|%DZiZV7TּCh?c>EjGs`CP=\h^&v[/;H<x[+Rѯ$.Q~m{a2#Xq*cmKMs'|C2jv,Vۘ'&غ3 ;`EYz/i- z^!=Ոʴgoqki-ʯ$y l7$Gn?>u DiMiDǫhf;87W*Q rR2 x1|3i{I`5 Q&3/aȌ 47drz?|:?t]nS :\: qxD3*9o6< Z)#KSYk_iKci":]4ps-w SNO/cTk{1jM,W,{ni ˷zB_3xĺ-grniBi?1EŕYI٣FO>_w"-:#o+$zuC.X6F'm|C>Ϧxz}>ou9L:r@``M? =i~!-%5]rlj7)or L3[D|6FCgKoy-xl%H_n =0{M|S> ޡkq4?cgVOڝʑ!w#nzgh&ŕR_$QY{B!i ^a~иe*67#j~6zuՄynYݾR|H~\6٣F4Kĺֵe]:kt:6MRIDfVٕp%jDJUT8|! gc8Fw:H;rW Ok߾kT~D%Vsj湍R f> n`qoSymK%B{+B T#>u|?Z?~SZκk Y~ mb]R@\If98 ;i @AMgq}sr 8.g,0T B%@?. G~SZ?Z?<%aWKvͿf~>dxٜ_QZ6>$5=cSlK+WKc&xwS}OtMN)MZlgn UTLك4I)G~SZ?Z?OGESSo[-aUeS,6`଍mc']|Z5Ms?[nS. . $3Oކ?-j}B֩-W Y?m-g|M>WoAk?o7Gmyn_oM5EE;ǎ[oFmX}ZQqмJ\[jm-p)88}S.,Lͷ76n Z}֛x +yndmt˔[kd3M9nY%d50OWA?~D2 Tgzq{%@3+U| Y?m-g|Mt/v1xpOugO˫ g$m ;wGUwkoP_=ޠeE5X AU$PUڑ(pvqw|m-g|Mͷ7{~!]/]n>ץj^\ldam`eX VhfZ?-6Kw\XM7 Q1pKq [9-;+,d+*趫7{k?H?KqڟT cf(C\Dr8/_}jJW+趮>?Կ_ ׇo- i.sijXZTOd`A {?̿:e`ܳ'ʪ$ Ry>:+|%tm;G#QFyĺ7-p6k$o%%RwgKsO5ö̇& MZKk[0iZc3A' ƈY?K; O5Gϝc xwj?K; O5Ec??AhWWڿ෷>$ /nw[h_n^ D6z<5V-_=MO232n$q,k>w2,yiƨT\|K3Rr;/IP'5}F"6ڭ2ھlȗ֡a*\7|7!=Ş&KONo#Q)Ha;2 w6'y_ZJ{yiƨT\V>eի&\6wZWSXޗ6Mᶒ9U&v6A"}o7mKǖv\WkAe̋m-,vWF)M,n l?K; O5Gϝ>egs6Z^ e{ds?4W k$pWZ>3oRzx#&;kMn Ʃ(:ѐ66ooTy_Z.>eiht .Kh5-*IC i<(mZ`Z|&Z5>-f|mD,DƣͩlGU 5ϝQqXsG^#U6]Xn]zVK{:G-q'`gbg|3·R{g jow:wjgZ~>wj?K; O5E |KF׵z'EJ3K8ET:mhv,/ WwV" yaq[6 A}P4W7^E|M2EpXC_5a}Ǘi$߿/ GDd2nmW|M2G!L$S`CCMt3yUߏ3vc+#9>Zxzv.-]KEҮm]Gy%@*P %1g)z)7^E0><9mBo%'pB<0ĤǗ+ʚ&\iAXh1/Ttb*Ǧ'7Mg)z)7^E?|~:^3YZx{Km,+v$i2 ?A ~ӭSP ИbL ̑E'$)f q>{g)z)7^E+y[9-;+,C>&KOH'o!Uq.PiBjɒD }0>^(uѿY̟EeT~Z|3-Yx;NÐ[sE8j\n8UC00`7i>?Xn5j/oᴝ⼲Kg%]3w D;5yVUp⾐:ҵ|Grs@DZgtz]Ŀ>+F^ҝc-'EMFX׋kwzH8 ښ%ĥ 6tWJ.mumet{/6Yt 6rLeW >TP)c(8>,V.y%ke[H`7He@[,ʠ.h3eȔcCA/D]KyI5.%1B̪ 6'vTXv*cCA/D %P-<ڄsavVFX8ⶨ ?ψ?"Q|AEX|Ah3eȕn.Oh3eȔcCA/Du^[#{dQrPFkd¶ۍYh3eȔcCA/D~,ᗎszR*)c917qFd Qv*cCA/D %[/Kj5oz|ɝ@|t6WT*9I3)gv:h3eȔcCA/DuZxZxT,Ѭ) 0 2#=+ %g*b>,5;J{g[m>4WWU %+Wr]Ue P]g(Ƈ> _V꾥u-us%A,"422f#s*@拰h3eȔcCA/DtQv*cCA/D %[S %g*]g(Ƈ> _V,TƇ> _G4?JEab4?J?ψ?"U( ?ψ?"Q|AEXMEZ{#S{x1#VG[ uլ' o|.Ϳq0YJoUs1XJouצҵ{۫k).9+8WBe[rO]'K,͋bXmJײp#}9xcf :2j&].դ]xk B[&_. oLR$y=s0߳׈/-*7:Mj-I?j/V3lS]ټsWp+GF;OZ=s]j7ZZ}[YmSdcxf<᮲?exJ?ڎjvfd2[4Q Gn]mB6ؚg<|Ҽa7Omᥞ ?nDzW,w2\OO$. fFDzw ɡ/m^uL7?ئPm<&[R} dy;pG{e U_ڎ<ڶ-}^D^Mh0h'h,ȸ1,RE j? z 7a65wRM<әmU;3? i(b1xWgNZV7:m픚]0o6v0Q@He|c=b?-M vZ.oXdӖZhD5DTf ͳ㯅> |UKW~*MCO/hzmYZ [v4QE+1_ ^k,׆4V lϧ4 ,e8#pWċGᦧ躥ϋ4x;v-8#lu$ g1I>Rgxg>&t>0]25bu:JaDź8n~R|K??KvϦE/٘lCM7QPf&'mY~Ǣ3 -_.;H:S^]AZhKk}.-<Wye·m42_aO y[.WYJ354RXŸ|=TUq]jKи"ـ[}l|gO2x!>ǟ _|{T,ˢNRVum'ڌc">E :_Y춉6ZTך$ZH~d{IyKCL $s&CC𝔚ޕ.mF,$[%Ra?* WմQXo|>};FԿ4u=[ykzE#|{ɷTj}:W_|Amej]>d$ KdX\FKuO:JehgiV]^l9bB#`v͌qҚl,axn}?ú]~ME,mc*G%IAMhևϝR>Ty_ZVEhy_ZTy_Z,}yiƨTX +CWxj0ɢjsOx*NѴOäksWQ []*H* ǘ^?xx~Oi궚eټxn6S˺ Nś634->Xb0[!VlhW+E$VH¼NKNJK J5ӿR)gǶa?ʨxD-d+*趧+??)XgLko\Db_΋a3KaEt-,$5$I,-S׵mW;5g ^UҼ7w&{yൔu`_oix /m3j$ޑX\J]=FJ0͙ۗPl!{G Zoе_?^_[WlQitͧIs*K x/UR[ɦ%$[Xc2Ȓ;y%%,+}B֩--j׃xC5]V|#E{%"KȝZ1Feh«M{_cvϤZ?~SZ}'㇌IsTOi5 6UHn1 M\>LbI`?Y?jK'TO{c_bm!nPUw-j}B֩-y"fWK{n_XZ dhlѦqr_UagNSĿWtky4ńu b@} {ӇwԴ(m,=~SZ?Z?~:|v^x4i'L׭[MBMVILFaV_N|m۟jOxbJc1lrNge-|G1v'<і߾kT~Okfx 9ǥh8X,mvEcaxaR1FL͑%Wssچk5cz\em5X Qnbid,jQ6#ls?Z?~SZ|W1ֹyoeiC3|<>~g}'9mסlqGw6otGYmQ|eZjz]sKwin7@1_w}}B֩--jׇ=#P͔!]3>|6ח1I)vPzw?lR]2}3MP :; 7_حd&y]Iu8Goе_?G ZEMAMuͦ Nj,[M9QuM֛v kr9E̖zX8 A +v^("hȰ&$V6azޘ'su?@Cw~kJ6r+sw}˱~n?G3׭i[&Qo@?o9Epy/3 /;_V i[&Qw,K /;9 zlﹿV ] .ǒ9 ~n?^o@?o9Glﹿ}˱~n?[>E_k h7_ lePl̠K#+g}+cw?}7ÐogG i&0FK }p]ӱfO"8/_}jǶa?ʨxD-ꆶ?&h0?d5JV%|???)XgLk|F uim8/m`5X'X#H൒7zPFΟjOCK`]HYlmr S f\l+.FR=]O|QZiz/]_j:ݞtg<=,h%/}h:yi>n|'[sO|-͕6 >[A*(vcDI+}~kW 4]S.n[GӬ)*%ɦ+'ڌxEϸ8(c<'i^,u_&u>UѢty/Øcx\iJy [h]7ծc5YC_`anE9_ %[E/7cO߷~1NW8gϞ!4 r OH >,udbnK< ޱ2U7*hU]?[. (:R[ʈ(bH#W)2 ]?Y]^լu4Z{fUVg+HʢH¨`ޑmKCE˿̾Kt_.mA?qqE! ZkNXu]"ltV{+eVU 8»@0&o7j?mEnabr>xǛ/~>c(o-?GImEkqb0 mp!hVU.f“w ^Wۿ<+f3g˿vP.N4S@+ VTPX$AʬH! FWm{k+,SwQ$nF]OPQ@|;}GU4mK/i|F8%,ePdlN]MYzDj4)>ut>u^_3HL;\R`4P; _Y W?w+~NwQoVƓAmWH[:=>Ua0"01ɮαޑg}K͵66^E'mb#>T_"~8F3φ>NmVҖ gg#)_1Y |dTP{]6{`Q=̑F"F34ڊ:Yx'òiioa&Yb@%XE1#ڢ1twvziR}qEg#%U6a4"G9Ұm4 +X,ie"$vG zbNI5b(Ѓ@-~^%@-3J;Y&D.]v $z)ЊU$g|Kڗ4.XthjbK_j$̡de(z'UkZw_խ#k_5Kf˕ɵa0I# _4on3~ Gu(mwM:N%glpsEpy&%ɋi^[?L^$ vzCr#)4KgmTw!R|aOïj:V L~ui峓*fɍr9EH~J; ,bcKњ($rd\)5GFX^%ީiK 6lQ`$!⏅>]OOxmgZ?j q;>JP *E)p_ ]kqmxGZ[c%X+32cy u_Og=Ϋ\Me)In!fBt$RI#kM/)~*&DS32~+fHE&xD! z Z|HH] .:#{mIfަqop\t?j]z]V-]6,ʘrw qpy};}[xFWH[:8V{+eVU4qwaM?~3xZjφ=Ju?*) %~֟(eIT'6o?B3tA$_ӣd[dP*(:!>b(R;W2Zar^08F$ Mv?rZwW?Yvxs_W oje.o*ȡnqO8#IѿY̟EeT{K^&,$HқuBs͌n9gXš]Ex}ĺu䶓',hdMJ8cg\i)\dk@~/4u~X[e-KݬkC.Voro-W&szMBvL#,AhOR?,(/x »Z)s1W)s^x柱ƽkCP7V։jmmaI.$M]I$C"ʪS z?[VbD-V4QEv솹׵JϮ\~@L*(fGf]`2# CO:?#l=*_i?_8G4/t+M q&W.JO:? '?h}+M q&W.JO:? '?h}+M q&W.JO:? '?h}+M q&W.Jğƚ\]J!<(!ڳM q&OO:? '?heC4I2Ova0eC4I2O?M q&y0I2O?M q&i?_8E_?M q&i?_8G4/tL/ q&i?_8G4/tO:?o&0<1okx vĚo|W<9}{tXCZݓITOi?_8G4/tix'|'G<FeC4I2Ov s3A?|u q&i?_8Eg> ~O???']z74/tO:?o ?O?O.x'|'^ '?heC4[/y< g ףI2O?M s3A? i1\Hˑ^ '?heC4kk$3ҫ%m\4/t~5/ k.丸(s @#ޥEaQEuspambayes-1.1a6/Outlook2000/docs/images/python.jpg0000664000076500000240000000552010646440135022044 0ustar skipstaff00000000000000JFIFMN"       }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?(((((dm.>FXt+x'ĺ5$/Q\dN]N9"̳Or^۷qWøOSKV躟exiٿu?Ygz^;k7t\+ CЊs+?Y9~m-/+e_cA} CĦYi"p%M¿7~οj+ؗ7~!~5xoƚU:༊!UEo(3^nPssV]n[>ul|&y:]ɻ*]^$eg_.;^/ ~*h _ BArl5ho!eUcx8WC9)+c{?du?~<Ծ ^'څƶw7K2 ,_oMV|?dψ?o85/ٻ_-k^'|A5ω41܍RdcQ|2`+}7O]=5׊W4|4108▿_xeڮAQ/{?H)KKbmYAl4H#??i[/Gt?+  c௙mg9aga#JqvY'v+=MLq[!bVpRW]ۏ*zkuCaYVYLB ^>_?n|_I5}߷7m7ܻ8GZ ~ķUËĿ| &Swp].#Y躍ךf-xce_"~__/cĿ~;>񦔊vH.m7$."d6>]Yr3) LNCCNWRJJ-5G^!zШN2qrtxfi/^4~$|p>K 0-ӭڭ7V%6&e38Eq)G>Go?:NJdxoK_k!Ӛsu~GyeA*h! 'Fg%E @)L$yb0è4G41 YdTbUGBE(RF 4Q:QVUWNV>6gVY)TW>FNN2mY4vR{?Fث6?sQ~:FyOhZ_]c6ԯ M|V;uY1V[O*vwAk{^uោ4tmRK,آ}2 Yf:>D²)j) "xX$hWؠ. `b./.X;ZOh𭖖erCGI6$ {- R^nJJ׽gmќx4:Ten߽ȒVwswdv>~k/?O |0\?ɬ[[sYL]>3SJ7_b;Ɩ׳EI-KN$r8&J?~jp~h&]7V%S!!K;B!T# & lٙx"&%@$rkJ7jJ]TCs uzUvMѴZOՏ'ůxï$ W| noÖd  sD(֖ hVE#\nʰ0s]߳yy_6ۘ1QY|3l9]Z/¿IiB4+eU"P0v@Q@U2ిyGY U2ిyGE_Y a/?O G}~t^ U2ిyGE_Y a/?O G}~t^ spambayes-1.1a6/Outlook2000/docs/images/sblogo.jpg0000664000076500000240000003375010646440135022016 0ustar skipstaff00000000000000JFIFM|"       }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?˪WUi}kZuZAXծ Oȁ>%F|-v;Gpa@Zhw~@ƒjk|dX϶Z~_z|~+MEޫj(%aNv(<1ҿgc}3r,c{'eg'&JfVIGOmO ~_.6EF}ַ2@c\6SOؖ42S?_ p=Ŷ1_|S76E8=1T+̧)+FtݶWIRඹ_ˢ}kC]50c d\v:OA]]A\E ,~ hJFyO!|mE/uHm(FGU~bC !7;ae->?@|? X [d蚤w6`>_iKSZ"엶׶%X#Њ㎉{ڗq6 2xj? D[[)[kqy_*^Z}ڲvZؤx1_¾i:knΕӑ@}ox#Qj~62C;WP6>cVH{Hmc5fvEi[_."[1oo28^!Xh8@hڜW1z|ѱ)ӓYAVR՛ln@1/ f?Uj}+ϗmh'֧WP6O]젌2y4], mKß \ofVPzU/П)^f|1]A5wyUU\U֢}VTlV}WΥb>kk">kk{ .hJ΋Z1ҼEGr_h?^>\2캥uVGֹ[.]UQGΑ?P|-~2~ϟhFuh*ÆVh-~3?%֭IR VSQ_>/Iߌ>*LLq:5א%*Ǎ[ݞ~}?i]{kgud7%α\dWG5r62!V*ZǖmJv$}m9dj.)^7V,ko 7 DѴS_ilԌ>{9{~ 7 X5#.],I~LU? awQu+-?Z[8<?_|_wrFZͿ +$y ܎q_SDnFqwZn{YF_ݼVRN3e 1cWMtwMJxWŸ~UHy,/vxt9[ZG,,QaW~7¿> -'^Đy]3޾gۻ^/k+[6ekwz;u ){p@.xc0|o-: 4k~I<ƶW $HyeHzW_Y~)t-sNd`~ hMGE>g"_çZRI'UqH>Wgq'4xEmtvwLO}9ewJ2JX||Uq oO$|GWuMpᶔ27\$`iЇ~x3şoį![hf>ChA&66/LuJU%)(kcE(ݞxx-{TȀ2 ETLs\}cyuOڶ'l$# b|}d6(%'1]\g 'L\ٵ(c{ lU]r8'9(*?hl G/ٟB֏&FOzFQHHPxOlǭo\ }èI?U|Zo./"jNҿN M?ÿ7llnɎ+#JG"% @Gď '~Вb{_P٨+i<'޺U)Ԍeimc(Efo|W/|>2FGks-ri` dpN*տW*ueq2[-6_^_ާƻZ]K-VO!\C+4@4$A!-cPX`vnin~YjGK_)/Z>2ǃӿ t &j)dfٗ㏁~׼Mol5[uo9T}cVm4n4@H,z j(g쯥mI[_ ' qW:~jAkt 6[Qda9j ѕS_| =N/[i.5ۤ@,τ/>:Iկ"MSPo%;QHfcӊf?k׾$XBڛAauM j$}/ 7[Gv*30ACN|JNJV)|akiȌ8M- KM Vீ?>#j_g\7z.LryKyoX+_*s~+gW_oIuiVUIe@9g~~<;6kywxc)E~ٟ<{C'ǯލM mjuhUb"Ĩ'cfZΣ/MX8Da'XnO֫i]oΈ_ w>֞0,{mned~ >%|_7gm yW7$ă'N+~.^WQ!TW?ƿ ۪Xx~( Ő[^\Ym<Ep 9?/ zGWQ 8IhRI#Zу~5WZ0}ƲTӨ+e+>H;_/QE_EϏtoo\._l;1<`.y^F6}#]ҭ&`[[H`Aݫs-7vw4-X:5 dЂs_!pNa8x +Qҵ+(9d;=k\[[[[TrV(0O͸¥,K)yeJ >֒]vZiԃo:ω4{B->n ]%ը`aRvrrzr*ދ q-Ef,Jt9c^߈>$-u O]ݤz]҈ՖXUbxJ><|1y˦?]p53 %(&wUh6?~|i>L$ڎy"09 tE5|yk+z6c^[{Ur2 Ycl_5oW>x eajW~͆Wknx^+ t,4֫B#v\bpH㌚? c|3EV?fsZ[o6R& c  ҿ /|752O%[H ?-^uy\C<ѼwIkxcGthZN>\D1q(#l Em%ۖes(5קcsD}VXadp78ۃ8n6K\ODNA=O%߁@}Ho'լoO_X-y'*.8P:e5M|áaſ  %_|&oP_Ho2"i:֡q=ï EUA_% KJO׾!9i #Y葼f\c! HRZ[|Vq+8HUZAݏSAE4t M:1ɌEc&b63@WZ_ceV+D׼|x-"k Z(iZB8_ÿ h[$%M~ei+bY*]jw7ao*o<񺜫) A:WQ>7;c_s=׏zn Yv,\mn: چ JEq9.6~B⍻iI*`s)(鵁s^Sim L3f6e;K O`3\/+Oߊ.?Z,A(*T~T(@)Gx30/~@GW%䧵;,n' 5' 4x@2Wݘ?Z!~)xTd]|Qt5$XcvunIΨ 㑆J4$xC~ӮT]Me_VZ_xoFjoxa,b6gp=+%j(4F^["-留de'_ƺ4V O:D6i!n¿K,5mP*8W5# u?>t$\8;XGb8ta?SxF}uMFBT#X`f0@?۷[R˝, =? !jamj-Kÿ 4h4QRˡIK~k9zִ`a-˪WUi}kZuZ|AXծ? ~Qg|ms;UA]M_¹k.|]~j~*I$dެ'2 (ҿ*$љ-euH@Vb܁ _g9]ØE[/VW&gFLDjlGxW7Z{PY"%d3QsܫWaJ?g#Fm'W4Kf'p}3_,jxaG=`vZ8ZN}utXd+{x5|ewe-FI#1+>2@Jb;ߴspi^rxj*R2ܤ2rN^8šjbjw6qEhxʫ, xȿ8*xWy<+c$dH';F=OYWx{V6qڣB!eeh>e5+Ԕ䶋sxЊJ7nP0W࠿ǁGxZS/,m:4x?^tR;kOH{6ǵ~T|TˡWs{5ItHKLIB4rnGL+g5w]zc*g(O }j^[قY2x~~-5{c>fuKQn3$20y?_Ŀ kP}#JuhvJ",mPx _ᆗ-xZ$n,pV2H0=Ex0,C*RI괶jGCEBl?য়5)OrcMRvx](G~/Zgo~l%7˃n⿐|~5A~ >\vkx S<7,:ޅA$r b `v+ȏgxyim. i06}._A"&| 9a/kxP.X%6\ʲЊH>hY]X$[ڥ0 0+/% j^To/!@oꡝb%Y-<7Wמ ?}o,5ٮ&6yl0F\2(d pH#_OػǞ!L4Gvg _¿;sck:^kZ\Ëvۘ _2hŭ&?Gf^+|wj 4릲KEp唂;]Wo<9o_׺xJMkO:1*"9J8)iҴ>2^w I4Bdh٧b$8V^]'{|&V7xMim8EbHibSS31zcgUU4%F6GG#'Dž|g]gW S/#ڻʲ@Qs׵x/ۖ/_ hhWR)oUīZS*i${:O\A"կ->fH+#9Hw5Z?e<iikv&px/ kׄ|IzΟzDM=7r8ا؁_g{mcĚ.H-d%-"(@]t2'6M?ٻ:V ׺Q\ףb!_I~O|/@t'b=+LY@$ӆ E q%MX9Z3v[±U`9LWXnҊI Uڥ]:{D{}kQ>}+ΙԶ,Gx>dGx>pOaEYukF?W?X^>Y~k[TӨ򺟷o6|`qE)#ό:ĞWÇ!ŧG#]E~'KɌgP:cF5i?xuO*x^ ?3dGcWSe?xq U~ܱgNjƅ*3hUɛ? ?>OID'ۅ*U,y Ht1_?Sjͣ"_U_&<;a-ݽ$ɯ૿Pp017Z1P:co#w=998%Ha2[5n1E ʀ0i?Q\ xsKzjiSV 䎸+GQs2h>HD۫"0R0cj_v2D@RN8ڿQLc/A1L F MYџH` f|+%u֡0Y!k[? z/?0GK N1ncz_#T /+Qn֗ԗdqJv$ ^mĿ%s`4h~M'7>^3x;_ ~^!&дuPB>bJJ79tF>ұR f{ON{vcSPj GV)?V?s1}xv 6zt7@Ō1<}k?b2Vդ7v#Al`8_M`~7@{xgJjpM?j;LxgJj|MRHK Y+]Ɓ#hzL ht8ՒiC: q _?d3Fh%a41d9N/ tn??SS]1"ƓՃLT/)aj?[Þӭ8u[ )B1_2x?׃l<=ir "20E# iAQT]Ɠȴ} c2ȵ8*NZy/U[4mqxxhVvpZK x#EIP0z ˸Ҵ]>Gd8(W wF\md/-aꮨ|=76Wh5蟱Nq5-uͲE L\m:cG"ԃ P^[HZyWk(K_WGĚ6އ {t(Ez/?MRM=>%Ih vq b1/O{--H]*WHZs/T*-LK?~*[ A\ضQ(z`T_W 6uV.^lѝI#5".`ix?_.qš?"3̵ܿBiYi:vԶ֒Q \T2  8>|yV$ vX?/?^+cosx1k?=c4'^o+(?S??Vu?kȔoj/2%c:Z?~b# lP@"Z7"S :~-+tZ$ ?wvW$/z"Z7"SW^?0F:c:/"WZLӒV?6ZOJ.]lKюȕ ?``~cCEJxOd͔ZV?Ԏ>kk }^=(hS/PW/"W44ڲCgE_n?,G'"Tî1f OC9TwCQ?1zִ` #:Hp5о뙴Ցspambayes-1.1a6/Outlook2000/docs/images/span.jpg0000664000076500000240000000124210646440135021461 0ustar skipstaff00000000000000JFIFM"       }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?((((ʳ"_2;spambayes-1.1a6/Outlook2000/docs/images/training.jpg0000664000076500000240000003376510646440135022352 0ustar skipstaff00000000000000JFIFC   %# , #&')*)-0-(0%()(C   (((((((((((((((((((((((((((((((((((((((((((((((((((p" SƷ+BܞӨ{:oף;=sX>ws/k?cG4gzT=R\S[ݞpJS󿿡~<]>Cۓ֎OT9o]8~#1 Z3ֈxRmu.U]ujY(ix;VYO^YZDV+] .ߥtfaVC˘WRqqqqqqqqqqqqq}g5K եY l+VѯĈĈĈĈĈĈĈĈĈĈĈĈĈĈĈğ< uhrDg4EX4E;WF KyĖriY-=TYE( 44h̾yRsNb̙I<45ڨ-x"<rҝ#PĈĈĈĈĈĈĈĈĈĈĈĈĈĈĈğ< *MPN*!C<ץw(UdGe$\yx'i皀@5,Zk6-(ܵiy:?v/`܇ y罛FSrfk' G @#Hfa34 xk##E EhĈĈ">`2U@G,f zzHfH^>/۸۸۸۸۸۸۸۸۸۸۸۸۸۸۸۸۸۸۸۸۸sp-14@!2P05 "#$353QTK]JjWyRʕԮwU ]B*nu۝v]juv]dev%]W`U*s\.a,5ʚrf\----Qy;]֢HQ# '))ڡyQoW )K5 IִUtu+]%BjH]Y.:N2̺.˭*늺⮼U*pHD\2.k5ŚMq[[[2[NmuQC),Hvh'񚗣?5l!۞dlz^*Z'ʪ޺ rJ;\c*Sd|F}w' hUP,6Ɇ 3‰ICO%xs=J1L0?*!dREmQ͓Sō/F~JыeM`X:u`X:u`X:u`X:U tttttttfҋ3P^ M 8bnI饕?'ߩ'qWrU$aJ4`u*C7RgQɬ̬̬̬̬̬̬̬̬̬̬̬̬̬̬̬Yj_-RڀTU(U S>5/F~z3Kџj^3Rg徫 ,$K ,$K &vvIa%XIa%XIyZ?0?Sy:wz3+S$#G?*#`Frz!q?hO?oӣiަ㩔4^:::::::::Mz3KіG~䋹"H.䋹"H.䋹"H.䋹"H.䋹"H.䋹"H.䋹"H.䋹"H.䋹"H.䋹"H.䋹"H#0"1?m9H.˲.˲.˲켋Ȼ/" g]fњ3Fq3Fq2Le(D&Q2Le(Ƒ_ Q#!1?E_'1iL' I¤Rp8T*^/ ׅkµZxV+^Epc/#˛l9Ûl9Û^^^^\g9/      E  !123"Aaq4Q#BR$@Pbr 0sDc?d8LZXՇaW r+9Ur*\ʮAW yU<Ur* ]ʮWarU\.Uv*]ʮWaUUV3XUc9UV3XUc9Uocx+zz*MvOM>JTdm_[_! dHS5'9#'9OT..L7ƢѭI1&v}WTqLqgIZ ) bbk栞 ) &K%bCs 9_͍n ;dq΢ֽvuN?}"a9"5٢k%2 O)8h iZ3ygb=ҪjxFn-~TQMu=1z!<<KjRnpT!7.U=:vN&l"/SA"sנQZ:kC]/xlL98fePu:Sm+Ec[ܜIUdC* ιxYJ#ңEţ@>2k-*mpO-2M-sLRkUnuVlhƬlMR.3?:T7:=w%1)Z+2?N .=KeJfD <+S.2! NC(0=RJxrH$f"\aLIE|G8-[7Tm3 `:I=-f=Π0m`jČt#TthMI@?^6Ÿ* ͌BwE#,GtXb;wE#,GtXb;wE#,GtXb;wE#,GtGj9Gj.tC=2C=2C=2C=2C=2C=2C=2]URkD꟱rFط?Y`FD+LJOFEvˊ<_ 7st1P h4*Zr'{FoFYku 5% E^e3Zt*-sd]/x"憵{e!n"t)`A/sDgIPb*¼x+^ Wx+^ Wx+^ Wx+^ Gj_;ܞ[i7A:mM1bS6mVR]D59ףVpZB͐b>-p+    a5@Q-FM.E6rS̢$H]TDwjLY^7nbi~jd[D,q鹶UQ=dtE bM׳H4ͤo iD4})Q{$`F+Ju"G^;L83eUA Q) ELipӄ7R)j HiK]s-\j^g^-M-9\-Ax$+8߄j9+ދœi {IKYFKkmSϟ*o0:EЧ{Zk[RΏZBq>/*<(\x%3]'H t]Jâ 3i#}:N6N4=d[%Hktt:*bss9S_Jrh4yP! E0c6bb~CVuq)=t!cGfA^O9tf B`aoVTX!Z >̞!xi\D{t( ;_2YXMa1&=kE/d*PK8v/?ύn *ǽb;wE#,GtXb;wE#,GtXb;wE#,GtXb;wE#,GtX6] WBЮt+OFط?~^20z7[a(R/)ӡQk&"~45) wH { Z'<>Vw9Vx+^ Wx+^ Wx+^ Wx+^ W;SvFXXA+)HR0!#ea546*e0HBŨ,+SvTmp& .P!r ,\`B (XBmpN)]ߘbQQ)myZ&~FnU''\-?,f&G iBxiGz6Ÿ( yTD#HK'v!%JR4k >l &N_5CZgb5 ťl*ƦhS'iJ5ȍfIkNY +XbD.n[ge+Fw\^FeƑY8qpvi;7NisSeEﴢc49?0i&7eP33\JgX?\n }\Y&+jbTY9d3#yOdEUKO֣l[!>kAPϊ +F/c-3n+'+&孕;!#HdNzZMs*&9ȓOH6 .5ӂ~R$jT3Tmςgzv(/l&ک]'o ΝKM/i3)&p~bNh59#M Fۤʼ~ "a|LQÌ{YQH~iڦj5RvJ=1';i3Tg9/ Yv++޻E!k|kmpU+NJӠm h[: ֍K-lmp66Ÿ}b>͍n tWOtWOtWOtWOt@0~tWOtWOtWOtWOt#l[~GD,sk4eP@Dϧ%@v^Vp}ݮbFc1 Lٌ(!N`?6J#aBxd+ե%kwV;Y uR d4MCt{OT=Wn=@4p͕^BϤ败MrlzGNɶm 葑lSI ϯݮb!Vtw8]wGstw8]wGsՉ_ ]hh.p ;.p ;.p׉:f6Ÿ}boՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄՄf@r*!1AQaq@P 0?!X0Y}zMYk7f٬ue al:X6As"?Хp}ZN98@Bp> >̪jYi w"*>3+d3!aKoŀP nUj(.8CQ 8y.`        #>Q*(Pu"QGkAXF檢Uh IEq+"g@$f_ݤlrc DQ]NE`uA"d볮, fDƋBu:*M0Ͻ:a`?#m젇 "dbEF0Y K"&kV@!D+_1lE 掇^ba*D`VQ*'TyX70. 2"@UT+Q#%+Z%戧b=ˆ|J`HEA!I }Њ!3EIfX0+r`i#m젗 cAT  zs8}8}8}8}8}8}8}8}8}8}8}8}8}8},Gs+%uq_AY+]\W|Fʈ+r6s'vP-xmyYt$ȌFuQ4E5?1 PT0O <3&Þt/8K[q5qP1 ^* Hv$D5ݢ@(dnϖ#A"nΓvtݝ&7gI:MnΓvtݝ&7gI:MnΓvtݝ&7gI:MnΓvtݝ&7gH@bSaS W BrD߀΁ v%SM* ȢL)*jC;5fG Jb`Vh[p%ALҬ1& 3}P~8e7/i{Mn^rܽ7/ig?,oe $LU!L"Yarcah"aZ{Ƕ'Pm!!J`0V 5uT` !}n2ִ*dd K 1 Nt D޴140BDE6IР8hibhT>ADA aDM]iGZP“$WT. Vi^ 64_6 Xg&Tوp@IPP"hD2y+X`r0ZdUdzժAx``l`AVgZB4˅"U*@X,B%QM( :#0ǠʨvP]W +b,}\0'p'p'p'p'p'p'p'p'p'p'p'p'p'p'p' X6 &7gY:nγvuݝf`xmy kq^2W`&0:YAԔf!}@ AĈS[ޕ .nΓvtݝ&7gI:MnΓvtݝ&7gI:MnΓvtݝ&7gI:MnΓvtݝ&7gI:B 8seQpc%r1S*uR@97(4h2'N'8Bd $I$I M{( h$66Q*^/J@HIDz FJ?8ƒ .!Xĉ|2iaء.kbEFk)*: EJnL0.C=``*>AȠT`b=wX11SފĆ@R!t U"qA­9y)p >LȀ ,`2|f1!uiJ&^0P<+C2z _idYː `\DC'Ȣ GЈ@P#r`BVifBI*tƛLi⨶AXKc{TNJ? L@ 6tnѫR`\Ih! җ3td$k`,"U:^JHcr0`hdm@+f 'PѶjՄ8hi[hAxFzhNacT[%4r&D ȓr,r,vm{(?]P~mvAOO)JR(L`/%)JRQ  _Ch&eeH!`?hoy??"@m젗P G"P늶v[ tA0j9JlEp芠0E /W"/EM)pJJhQ\[ajcP^cQӌ$նPG@ >xǼ=7y{oXlHq:!Ǽ=7y{oxǼK<6kr&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&r&P& 'mjʞQByeݖq}m|{6Í}}} u4q=vL}c]:}}}{߻/}}}mw}7}}}}}}}}}}}}}?}}}}}O<<<<<:a!A @P10Q?t[LRiiiiiiiiiiiiaaia`2fq2sp'k&LѢ'G׏7]} c1c1c3"!1@A0q Q?ގI/&mT[ H sJŸG<Z>ki1 ydt]S;# SL0`P0c;/|<0`["! ["!Byuo[d:uò((((()!1AQa 0@Pq?\8;+"|27? 8G^x\᳇F_'sv/|~p_|<؜d'LA&@}}} 3˲6U(?:(N(QR Ԛ.ƜP4Vrbg g/];+EFNr4E\O!E] E7 axXn*%6i, 92p\ϫ@ 48P2!B́,Xq`ɇ<80Xgp,@8pwl`;|`W?`>wߌ~0#_z^́Un<28R~+>Rt9¼HS#q@,ۊbLb҅ O\ Ȫj`f (|t1K!g2%:)T .NdHÅ5j`;bGdЈfc;!"+ AƸj+C36VU/8+c80#Qn P Jjbb S%@ DQ%d(~`\!͌Z-#`5aatndZt)qd1c{ALyKG=~ܠDA݅fPpIo adc%;:#Yu-zȅgd  }؆i9kR0n'7pK)[3Ќ PQtl@z-8HH{d1D0U@ݠ4`lgF4,*# {zb6VI8cIt:*ypQ[ZU\i: hhbv)՘*6, ӻ/s(xPBQ8\1"%w `D~߿~߿~߿~'9*!EjYxּKAp[Z.b-InkĹ-'"xּKAp[Z.b-InkĹh)]ڼ Ϩ2= hrq*~Q9 :O"?tY,yԹvstg8_Q84菢" ]JO9F3\$H> ɄAF@*=FhЂ=m'q>^ pmdyx^^//yx^^//yx^^//yx^^//OK0EVPJJrΈzYcӼKv4eߨdaTP@@ʘo&d,&Vhr%IKЬ;tC^*^$SDnGײ?c"(tL0` /%TU>3Ш`2CHuM䒥$D۞E2`4%:xgPєH .,G¢azHddt c Ӕu :rϛNFł5PsLĪJSU5ı]4%U/&)PLPcv11`@zm )mbW.ʆ P6:RnXj,4"j -G8)R8Ms.XJGGB:!E9a*ءBϠM9Re0( ,[BIW,a, i@2+@DKCET;%$ZcMޒnL7l-*=ˀ(u0vnZ5vJgk$=%kh Tש[0j8/bAFu]PDUɡC{v,F\djH!=M@"K%;IJ*7TSKR JX>0eBX4L\:LH2:үh48f._(VcK؀kdϠ*EA D`߿~߿~߿~߿~.y^o/7yy `{V~~N .#躆RQW a*&l geр/)v^Sk yx^^//yx^^//yx^^//yx^J}@}E띩j}b9{j(iu^^*l/Tꀪmԩڔ*zms1wOP`>$+D)ߠ=浊 ":"dHIsg^ <[`ÊHp5$@zBO6IN;:b #9BU`QXh.sx%H" Bd'(Sϳ(]DvIzϣ)k64֛`ۤ% cj*8,8f' 0V [j(֚Q{AC@TsͳZzU_Ȝl%B1U*UȜl%B1U*Uv^»/s]pWe~+>p )b''S666666666ø@DNe~BS$2]rвwyFO=_I߽ HxHT/s KQiKn4Q|:*0VZ9P fRq&Ԅ Gq=_I߽lI Q*-' xp0R5:q)v^P&D67]yu3N:tB8rgͮ'7MM;B{TӧN:tIN;/s](BX~۶mmmmmmmmmmm]=spambayes-1.1a6/Outlook2000/docs/setup.py0000775000076500000240000000776711116610077020305 0ustar skipstaff00000000000000#!/usr/bin/env python """setup.py Generate any dynamic documentation for the Outlook plug-in. Typically, this involves options that can be set - dynamic generation means that the information needs only be updated in one central location, and the documentation can still stay up-to-date. """ # This module is part of the spambayes project, which is Copyright 2002-5 # The Python Software Foundation and is covered by the Python Software # Foundation license. __author__ = "Tony Meyer " __credits__ = "All the spambayes folk." import os import sys # Fix path so we can import from Outlook2000 directory. sys.path.append(os.path.dirname(os.path.dirname(__file__))) import config from spambayes.Options import defaults from spambayes.OptionsClass import OptionsClass from spambayes.OptionsClass import PATH, INTEGER, REAL, HEADER_NAME # Replace common regexes with human-readable names. # If the value is None, then skip those options, as they are not # human-editable. nice_regex_names = {PATH : "Filename", INTEGER : "Whole number", REAL : "Number", HEADER_NAME : "Email Header Name", config.FOLDER_ID : None, config.FIELD_NAME : "Alphanumeric characters", } table_header = """ Available options
Logo  

Available options

""" def main(): outlook_config = config.CreateConfig() spambayes_config = OptionsClass() spambayes_config.load_defaults(defaults) # Create HTML pages that outline the available options. for fn, o, sects in [("outlook-options.html", outlook_config, ("General", "Filter", "Training", "Notification")), ("spambayes-options.html", spambayes_config, ("Tokenizer", "General", "Classifier", "Storage"))]: f = open(fn, "w") f.write(table_header) for sect in sects: f.write(' \n') opts = o.options_in_section(sect) opts.sort() for opt_name in opts: opt = o.get_option(sect, opt_name) # Skip experimental and deprecated. if opt_name.startswith("x-"): continue # Replace regex's with readable descriptions. if opt.allowed_values in nice_regex_names: replacement = nice_regex_names[opt.allowed_values] if replacement is None: continue opt.allowed_values = (replacement,) f.write(opt.as_documentation_string(sect)) f.write('\n') f.write("
Section Option Name Valid Values Default Comments
\n") f.close() # Create pre-filled configuration files with comments. for fn, o in (("outlook-defaults.ini", outlook_config), ("spambayes-defaults.ini", spambayes_config)): f = open(fn, "w") f.write(o.display(add_comments=True)) f.close() if __name__ == "__main__": main() spambayes-1.1a6/Outlook2000/docs/troubleshooting.html0000664000076500000240000004651510646440135022702 0ustar skipstaff00000000000000 Troubleshooting the SpamBayes Outlook plugin
Logo  

Troubleshooting the SpamBayes Outlook addin

This is a list of common problems, and hopefully their solutions. Please feel free to suggest additional topics. Currently, we have the following problems listed:

Some other resources that may be useful in tracking down any problems:

If you must send someone a mail about SpamBayes, please read this first.

Toolbar items appear, but fail to work

If the toolbar items fail to work, we are facing one of two problems.

  • The addin has failed to load. In this case, along with the toolbars failing to work, SpamBayes will not be filtering or scoring any messages. To fix this, see the Addin doesn't load instructions.
  • If the addin has loaded (ie, is filtering and scoring mail) but the toolbar items don't, we have struck a common problem with the toolbars. Follow the instructions below.

First we will try deleting the toolbar, and if that fails, completely reset all Outlook toolbars. Perform the following steps:

  • Right-click on any Outlook toolbar, and select Customize.
  • In the dialog that appears, ensure the Toolbars tab is selected, locate SpamBayes in the list of toolbars, and select it.
  • Click on the Delete button. Outlook will ask for confirmation that you want to delete the SpamBayes toolbar. Select OK.
  • Close the customize dialog. The SpamBayes toolbar no longer appear.
  • Restart Outlook. SpamBayes will re-create the toolbar.

If all else fails, you can completely reset the Outlook toolbars by removing the file \Documents and Settings\{username}\Application Data\Microsoft\Outlook\outcmd.dat Although this is undocumented by Microsoft, we have never heard reports of problems. If you are paranoid, simply rename this file so that you have a copy.

Addin loads with an error message

In this case, when you start Outlook you receive a message indicating that SpamBayes could not be initialized.

This means that SpamBayes has loaded, but struck an error during initialization. If the information in the error message does not indicate the nature of the error, please report a bug (making sure you attach the log file).

Addin doesn't load

If you start Outlook but there was no error message, the SpamBayes toolbar items do not work and new messages have no spam score or filtering applied, then the plugin has probably become disabled.

  • Check the log file. If a log file for this session exists, then see if it contains an error. If not, check the date and time of the log - it is probably a log from the last time it did work, so is no help to us. If a log does exist, please report a bug.
  • Check that Outlook shows the addin as enabled.
  • If you are running from source code, the addin will not appear in the steps below. Please re-register the addin, as per the README.txt file.

    1. Start Outlook, and select Options from the Tools menu to display the main Options dialog.
    2. Select the tab labeled Other, then click on the Advanced button.
    3. Click on the COM Add-Ins button.
    4. If the SpamBayes addin is not listed, then SpamBayes should be reinstalled (Note that running regsvr32.exe outlook_addin.dll or bin\outlook_addin_register.exe from the SpamBayes directory may also solve this problem).
    5. If the SpamBayes addin is listed but not checked, then simply check it and close the dialog.
    6. If you are running Outlook XP/2002/2003, you may find that if you go back to the dialog, the addin will still be unselected. In this case, perform the following:
      1. Select About Microsoft Outlook from the Help menu.
      2. Click the Disabled Items button.
      3. Select SpamBayes.
      4. Click Enable.
      5. Restart Outlook.

    If none of that works, please report a bug.

    Messages fail to filter

    This is when messages arrive, but have no spam field value. Note that this is different from a message having an incorrect or unexpected spam value. This is for messages that have a completely blank spam score. To resolve this:

    • Check that filtering is enabled. Select the SpamBayes Manager, then ensure the button Enable Filtering is checked. If you are unable to select this button due to insufficient training information, please review the initial configuration documentation for information on training.
    • If only the occasional message fails to filter, then it is likely that the message is in a format we don't understand. There is almost certainly an error listed in the log file. Please report a bug, attaching both the log file and the message that caused the error.
    • If all messages fail to filter, we have a more serious problem but again, please report a bug, attaching the log file.

    Messages have incorrect or unexpected spam values

    This is when filtering appears to work OK, except that the spam values are wrong. To resolve this:

    • If the messages are all scoring as "unsure", with a score of 0.5, then you may have lost your training database. From the SpamBayes Manager dropdown, check how many spam and good messages have been loaded by the system. If this number is very low (like zero!) then you probably need to perform a full re-train of your database.
    • If the messages have apparently random, but unexpected scores, then there are two possibilities; either SpamBayes is simply behaving what appears to be strangely, but really is correctly, or that some of the spam payload is invisible to SpamBayes. In both cases, perform the following:
      • Ensure the message is selected in the Outlook preview pane, and from the SpamBayes Manager dropdown, select Show Spam clues for current message. This should open a new mail message with the clues.
      • Part of the clues shows the body of the spam message. If this message correctly shows the spam text, then it is likely SpamBayes is behaving correctly. In this case, you may wish to mail the clues to the SpamBayes mailing list for help in decoding the clues, but it is likely that SpamBayes is behaving correctly given your current training data.
      • If it appears that part of the spam payload is missing, then you have probably stumbled across a bug - please mail the clues to the mailing list.

    Resetting SpamBayes configuration

    In some cases, it may become necessary to reset your SpamBayes configuration, especially if your configuration becomes invalid. SpamBayes attempts to detect this situation, but doesn't always get it right. This section details where critical configuration files are stored - more detailed information is also available.

    SpamBayes stores all configuration data in your data directory. The configuration information is stored in a file called [profile name].ini, where profile name is the name of your Microsoft Outlook profile. The default profile name is usually Outlook or Microsoft Outlook Internet Settings, but Outlook can be configured to use any number of profiles, with any name.

    Note that, in this directory, you may also find a file named default_bayes_customize.ini - this file is not used to configure the Outlook side of SpamBayes - look for any other .ini files in that directory.

    If you delete the configuration file, SpamBayes will be completely reset. Note you will not lose your training data, only your configuration information. The next time you start Outlook, the SpamBayes configuration wizard should appear, guiding you through the configuration process

    Your training data is also stored in this directory, but in files with a .db extension. If you ever want to delete your training information, remove the two .db files in this directory.

    You may like to consider backing up this directory.

    SpamBayes is not available for all users on the machine.

    When SpamBayes is installed, by default it is available only for the user who installed it. This is to allow SpamBayes to appear in Microsoft Outlook's COM-Addin list, and therefore able to be activated and de-activated by the user inside Outlook.

    It is possible to register the addin so it is available to all users on a particular machine, which can be useful in enterprise arrangements where users have 'roaming profiles'.

    To register SpamBayes in this way, you must log on as a user with permissions to modify the system registry, then execute the command (with the correct path substituted):

    "c:\Program Files\SpamBayes\bin\outlook_addin_register.exe" hkey_local_machine

    Note that the double-quotes in the above command are significant (and should be typed). Because "Program Files" has a space in it, you must surround the entire command name with quotes. Otherwise, you'll get an error something like:

    'c:\Program' is not recognized as an internal or external command, operable program or batch file.

    If you check the installation log after performing such an install, you should see the following messages:

    Registered: SpamBayes.OutlookAddin
    Registration complete.
    Registration (in HKEY_LOCAL_MACHINE) complete.

    Note the last line, which does not exist when registration is performed only for the current user. Once you have performed this registration, the Addin will be available for all users - but as noted above, it will no longer appear in Outlook's COM-Addin list.

    All other problems

    If you are simply unsure about what SpamBayes is doing, please send a mail to the SpamBayes mailing list with as much information as possible. If you are fairly sure you have struck a bug, then please report it. Please do not mail any of the contributors directly.

    Process Descriptions

    This explains some of the processes above in more detail.

    Determine your installation type.

    If you are running from Python source code, and installed Python, plus SpamBayes as separate components, then you are running the source code version. If you downloaded an installer .exe file, then you are running the binary version.

    Check the log file

    Determine your installation type. If you are running the source code version, then please see README.txt in the Outlook2000 directory.

    If you are running the binary version, the simplest way to get hold of the most recent log is to:

    1. Open the SpamBayes Manager dialog (from the SpamBayes toolbar).
    2. Click the Advanced tab.
    3. Click the Diagnostics button.
    4. Click the View log button.

    To find the log manually, you'll need to find your Windows temp directory, into which the SpamBayes addin writes the log. This directory is generally \WINDOWS\TEMP for Windows 95, 98 and ME, or \Documents and Settings\{username}\Local Settings\Temp for Windows 2000/XP.

    Note that by default, in Windows 2000 and XP, Windows Explorer will not show the Local Settings directory. You can convince Windows Explorer to show this directory (and therefore allow you to see the Temp directory under it) by doing either:

    • Select the folder \Documents and Settings\{username}.
    • This directory should be reflected in the Address Bar.

    • In the Address Bar, simply type, at the end, \Local Settings (thereby giving the full path name), and press Enter.
    • Windows Explorer will then show this folder, and you can open the Temp folder, which is in it.

    or

    • Select Folder Options from the Tools menu.
    • Select the View tab.
    • In the list, select Show hidden files and folders.
    • Select OK.
    • The temp folder will now be visible, and you can then open it. You may like to then reset this option back to the default value.

    The log file for the most recent execution of Outlook is named spambayes1.log, the second most recent is named spambayes2.log, and so on for the four previous runs. You can view this file with Notepad. Usually, you will simply see messages which indicate that SpamBayes is doing its job; however in some cases there will be errors in this file. If there are errors, please report a bug.

    If the log file is very large

    This probably means that SpamBayes failed to process a large number of (or a few, large) emails. In that case, please perform the following steps:

    • Ensure all messages in your watch folders are marked as read.
    • Restart Outlook (use Exit and Sign off if it is in your File menu)
    • Send yourself a test message, and wait for it to arrive.
    • Exit Outlook.

    You should have a new log file containing the error when classifying the test message. If no error occurs processing the test message, the previous large log file will still exist (see above). Either edit the file using a text editor to extract just the error information, or zip it up. If you don't know what that means, please send a mail.

    Locating your Data Directory

    SpamBayes stores all configuration and database information in a single directory. By default, this directory is located under the user's Application Data directory. You can locate this directory by using the Show Data Folder button on the Advanced tab of the main SpamBayes Manager dialog.

    If you need to locate it by hand, on Windows NT/2000/XP, it will probably be C:\Documents and Settings\{username}\Application Data\Spambayes, or on other versions of Windows it will probably be C:\Windows\Application Data\Spambayes.Note that the Application Data folder may be hidden, so Windows Explorer may not show it by default, but you can enter the path into the Address Bar and Explorer will open it.

    Note that by modifying the configuration files, you can tell SpamBayes to store this data in any directory, so it is possible your data is being stored elsewhere - contact your network administrator if this appears to be the case.

    Report a bug

    All SpamBayes bugs are maintained in on a page at sourceforge. Please have a check of the bugs already reported to see if your bug has already been reported. If not, open a new bug, making sure to set the Category to Outlook. Please ensure you attach the log file to the bug.

    If you are unsure about the bug, or need any assistance, please send a mail.

    Send a mail

    If all else fails, you may want to send someone a mail. Please make sure you have read this document thoroughly before doing do.

    Your mail should be sent to the SpamBayes mailing list (spambayes@python.org). Please do not mail any of the contributors directly! (see "good karma" below).

    Please ensure this mail contains:

    • the version of Windows you are using,
    • the version of SpamBayes,
    • any log files.

    If you also mention that you read this trouble-shooting guide and are still stuck, then you will be more likely to get answered! (And if you can subscribe to this mailing list and help answer other questions, and good karma will come your way!)

    spambayes-1.1a6/Outlook2000/docs/welcome.html0000664000076500000240000001443510646440135021102 0ustar skipstaff00000000000000 Welcome To SpamBayes
    Logo  

    Welcome to SpamBayes

    SpamBayes is an Outlook plugin that provides a spam filter based on statistical analysis of your personal mail. Unlike many other spam detection systems, SpamBayes actually learns what you consider spam, and continually adapts as both your regular email and spam patterns change.

    When you first start Outlook after SpamBayes has been installed, the SpamBayes Installation Wizard will appear. This Wizard will guide you through the configuration process and allow you to quickly have SpamBayes filtering your mail. This document contains additional information which will help make SpamBayes effective from the first time you use it.

    Please remember that this is free software; please be patient, and note that there are plenty of things to improve. There are ways you can help, even if you aren't a programmer - you could help with this documentation, or make a donation, or any number of other ways - please see the main project page for information. The list of Frequently Asked Questions may also answer some of yours.

    For more information on SpamBayes, including links to the rest of the documentation, see About SpamBayes.

    Training

    SpamBayes has no builtin rules, so anything you consider spam will be treated as spam by this system, even if it does not conform to the traditional definitions of spam. This means that SpamBayes requires training before it will be effective. There are two ways that this training can be done:

    • Allow SpamBayes to learn as it goes. Initially, SpamBayes will consider all mail items unsure, and each item will be used to train. In this scenario, SpamBayes will take some time to become effective. It will rarely make mistakes, but will continue to be unsure about items until the training information grows. This is the training technique that we recommend you use.
    • Pre-sorting mail into two folders; one containing only examples of good messages, and another containing only examples of spam. SpamBayes will then process all these messages gathering the clues it uses to filter mail. Depending on how many messages you train on, SpamBayes will be immediately effective at correctly classifying your mail.

    The Installation Wizard will guide you through these options.

    It is important to note that even when SpamBayes has little training information, it rarely gets things wrong - the worst it generally does is to classify a message as unsure. However, as mentioned, the more training information SpamBayes has, the less it is unsure about new messages. See using the plugin, below, for more information.

    Using the Plugin

    SpamBayes Toolbar

    This section describes how the plugin operates once it is configured. You can access the SpamBayes features from the SpamBayes toolbar (shown to the right), but in general, SpamBayes will simply and silently filter your mail.

    As messages arrive, they are given a spam score. This score is the measure of how "spammy" the system has decided a mail is, with 100% being certain spam, and 0% meaning the message is certainly not spam. The SpamBayes addin uses these scores to classify mail into one of three categories - certain spam, unsure, and good messages. Good messages are often known in the anti-spam community as ham.

    Mail that is classified as good is not touched by this plugin by default - they will remain in your inbox, or be filtered normally by Outlook's builtin rules. Typically, and by default, mail that is classified as either unsure or certain spam is moved into different folders for future review.

    Outlook does not allow us to automatically add the spam score to your Outlook folder views - but you can do it manually by following these instructions.

    There are three ways in which the system can get things wrong:

    • A spam stays in your inbox. This is known as a false negative. In this case you can either drag the message to the spam folder or click on the spam button on the Outlook toolbar. In both cases, the message will be trained as spam and will be moved to the spam folder.
    • Any message is moved to the unsure folder. In this case, the system is simply unsure about the message, and moves it to the possible spam folder for human review. All unsure messages should be manually classified; good messages can either be dragged back to the inbox, or have the Not Spam toolbar button clicked, while spam messages can either be dragged to the spam folder or have the spam toolbar button (shown above) clicked. In all cases, the system will automatically be trained appropriately.
    • A wanted (ham) message is moved to the spam folder. This is known as a false positive. In this case you can either drag the message back to the folder from which it came (generally the inbox), or click on the Not Spam button. In both cases the message will be trained as good, and moved back to the original folder.

    Note that in all cases, as you take corrective action on the mail, the system is also trained. This makes it less likely that another similar mail will be incorrectly classified in the future.

    spambayes-1.1a6/Outlook2000/export.py0000664000076500000240000002050010646440136017513 0ustar skipstaff00000000000000# Exports your ham and spam folders to a standard SpamBayes test directory. import sys, os, shutil from manager import GetManager NUM_BUCKETS = 10 DEFAULT_DIRECTORY = "..\\testtools\\Data" import re mime_header_re = re.compile(r""" ^ content- (type | transfer-encoding) : [^\n]* \n ([ \t] [^\n]* \n)* # suck up adjacent continuation lines """, re.VERBOSE | re.MULTILINE | re.IGNORECASE) # Return # of msgs in folder (a MAPIMsgStoreFolder). def count_messages(folder): result = 0 for msg in folder.GetMessageGenerator(): result += 1 return result # Return triple (num_spam_messages, # num_ham_messages, # ["Set1", "Set2", ...]) # where the list contains one entry for each bucket. def BuildBuckets(manager, num_buckets): store = manager.message_store config = manager.config num_ham = num_spam = 0 for folder in store.GetFolderGenerator(config.training.spam_folder_ids, config.training.spam_include_sub): num_spam += count_messages(folder) for folder in store.GetFolderGenerator(config.training.ham_folder_ids, config.training.ham_include_sub): num_ham += count_messages(folder) dirs = ["Set%d" % i for i in range(1, num_buckets + 1)] return num_spam, num_ham, dirs # Return the text of msg (a MAPIMsgStoreMsg object) as a string. # There are subtleties, alas. def get_text(msg, old_style): if old_style: email_object = msg.OldGetEmailPackageObject() else: email_object = msg.GetEmailPackageObject() try: # Don't use str(msg) instead -- that inserts an information- # free "Unix From" line at the top of each msg. return email_object.as_string() except: # Fudge. GetEmailPackageObject() strips MIME headers by default. # I'm not exactly sure why, but I have some spam with what looks to # be ill-formed MIME, such that the email pkg's .as_string() (or # str() -- same thing, really) gets fatally confused when the MIME # headers are stripped, dying with an internal # # string payload expected: # # TypeError. Ignore the exception and try again. pass # This is what our ShowClues() does, and that's never had a problem # getting a string from these problem messages. email_object = msg.GetEmailPackageObject(strip_mime_headers=False) text = email_object.as_string() # If we leave the Content-Type and Content-Transfer-Encoding headers in # now, the email package can get confused when it tries to parse this # string. So, alas, strip 'em by hand. i = text.find('\n\n') # boundary between headers and body if i < 0: # no body i = len(text) - 2 headers, body = text[:i+2], text[i+2:] ##print 'before:\n', text headers = mime_header_re.sub('', headers) # remove troublesome headers text = headers + body ##print 'after:\n', text # A sanity check, to make sure the email pkg can still parse this mess. # If it can't, it will raise some exception. I haven't seen this # happen yet. Getting into this section is rare (less than 1% of my spam # so far), so the expense doesn't bother me. import email email.message_from_string(text) return text # Export the messages from the folders in folder_ids, as text files, into # the subdirectories whose names are given in buckets, under the directory # 'root' (which is .../Ham or .../Spam). Each message is placed in a # bucket subdirectory chosen at random (among all bucket subdirectories). # Returns the total number of .txt files created (== the number of msgs # successfully exported). def _export_folders(manager, root, buckets, folder_ids, include_sub, old_style): from random import choice num = 0 store = manager.message_store for folder in store.GetFolderGenerator(folder_ids, include_sub): print "", folder.name for message in folder.GetMessageGenerator(): this_dir = os.path.join(root, choice(buckets)) # filename is the EID.txt try: msg_text = get_text(message, old_style) except KeyboardInterrupt: raise except: print "Failed to get message text for '%s': %s" \ % (message.GetSubject(), sys.exc_info()[1]) continue fname = os.path.join(this_dir, message.GetID()[1]) + ".txt" f = open(fname, "w") f.write(msg_text) f.close() num += 1 return num # This does all the work. 'directory' is the parent directory for the # generated Ham and Spam sub-folders. def export(directory, num_buckets, old_style): print "Loading bayes manager..." manager = GetManager() config = manager.config num_spam, num_ham, buckets = BuildBuckets(manager, num_buckets) print "Have", num_spam, "spam and", num_ham, "ham to export,", print "spread over", len(buckets), "directories." for sub in "Spam", "Ham": if os.path.exists(os.path.join(directory, sub)): shutil.rmtree(os.path.join(directory, sub)) for b in buckets + ["reservoir"]: d = os.path.join(directory, sub, b) os.makedirs(d) print "Exporting spam..." num = _export_folders(manager, os.path.join(directory, "Spam"), buckets, config.training.spam_folder_ids, config.training.spam_include_sub, old_style) print "Exported", num, "spam messages." print "Exporting ham..." num = _export_folders(manager, os.path.join(directory, "Ham"), buckets, config.training.ham_folder_ids, config.training.ham_include_sub, old_style) print "Exported", num, "ham messages." def main(): import getopt try: opts, args = getopt.getopt(sys.argv[1:], "hqon:") except getopt.error, d: usage(d) quiet = 0 old_style = False num_buckets = NUM_BUCKETS for opt, val in opts: if opt == '-h': usage() elif opt == '-q': quiet = 1 elif opt == '-n': num_buckets = int(val) elif opt == '-o': old_style = True else: assert 0, "internal error on option '%s'" % opt if len(args) > 1: usage("Only one directory name can be specified.") elif args: directory = args[0] else: directory = os.path.join(os.path.dirname(sys.argv[0]), DEFAULT_DIRECTORY) if num_buckets < 1: usage("-n must be at least 1.") directory = os.path.abspath(directory) print "This program will export your Outlook Ham and Spam folders" print "to the directory '%s'" % directory if os.path.exists(directory): print "*******" print "WARNING: all existing files in '%s' will be deleted" % directory print "*******" if not quiet: raw_input("Press enter to continue, or Ctrl+C to abort.") export(directory, num_buckets, old_style=old_style) # Display errormsg (if specified), a blank line, and usage information; then # exit with status 1 (usage doesn't return). def usage(errormsg=None): if errormsg: print str(errormsg) print print """ \ Usage: %s [-h] [-q] [-n nsets] [directory] -h : help - display this msg and stop -q : quiet - don't prompt for confirmation. -n : number of Set subdirectories in the Ham and Spam dirs, default=%d Export Spam and Ham training folders defined in the Outlook Plugin to a test directory. The directory structure is as defined in the parent README-DEVEL.txt file, in the "Standard Test Data Setup" section. Files are distributed randomly among the Set subdirectories. You should probably use rebal.py afterwards to even them out. If 'directory' is not specified, '%s' is assumed. If 'directory' exists, it will be recursively deleted before the export (but you will be asked to confirm unless -q is given).""" \ % (os.path.basename(sys.argv[0]), NUM_BUCKETS, DEFAULT_DIRECTORY) sys.exit(1) if __name__=='__main__': main() spambayes-1.1a6/Outlook2000/filter.py0000664000076500000240000001746611116563002017467 0ustar skipstaff00000000000000# Filter, dump messages to and from Outlook Mail folders # Author: Sean D. True, WebReply.Com # October, 2002 # Copyright PSF, license under the PSF license # Action texts could be localized. # So comparing the action texts should be done using the same localized text. # These variables store the actions texts in the same localized form how the # user sees them in the Action dropdowns from configuration dialogs ACTION_MOVE, ACTION_COPY, ACTION_NONE = None, None, None def filter_message(msg, mgr, all_actions=True): config = mgr.config.filter prob = mgr.score(msg) prob_perc = prob * 100 if prob_perc >= config.spam_threshold: disposition = "Yes" attr_prefix = "spam" if all_actions: msg.c = mgr.bayes_message.PERSISTENT_SPAM_STRING elif prob_perc >= config.unsure_threshold: disposition = "Unsure" attr_prefix = "unsure" if all_actions: msg.c = mgr.bayes_message.PERSISTENT_UNSURE_STRING else: disposition = "No" attr_prefix = "ham" if all_actions: msg.c = mgr.bayes_message.PERSISTENT_HAM_STRING ms = mgr.message_store try: global ACTION_NONE, ACTION_COPY, ACTION_MOVE if ACTION_NONE is None: ACTION_NONE = _("Untouched").lower() if ACTION_COPY is None: ACTION_COPY = _("Copied").lower() if ACTION_MOVE is None: ACTION_MOVE = _("Moved").lower() try: # Save the score # Catch msgstore exceptions, as failing to save the score need # not be fatal - it may still be possible to perform the move. if config.save_spam_info: # The object can sometimes change underneath us (most # noticably Hotmail, but reported in other cases too). # Retry 3 times handling ObjectChanged exception. # Why 3? Why not! for i in range(3): try: msg.SetField(mgr.config.general.field_score_name, prob) # and the ID of the folder we were in when scored. # (but only if we want to perform all actions) # Note we must do this, and the Save, before the # filter, else the save will fail. if all_actions: msg.RememberMessageCurrentFolder() msg.Save() break except ms.ObjectChangedException: # Someone has changed the message underneath us. # The general solution is to re-open the message, and # try again. We reach into our knowledge of the # message to force this. mgr.LogDebug(1, "Got ObjectChanged changed - " \ "trying again...") msg.dirty = False msg.mapi_object = None # cause it to be re-fetched. else: # Give up trying to save the score. mgr.LogDebug(0, "Got ObjectChanged 3 times in a row - " \ "giving up!") msg.dirty = False except ms.ReadOnlyException: # read-only message - not much we can do! # Clear dirty flag anyway mgr.LogDebug(1, "Message is read-only - could not save Spam score") msg.dirty = False except ms.MsgStoreException, details: # Some other error saving - this is nasty. print "Unexpected MAPI error saving the spam score for", msg print details # Clear dirty flag anyway msg.dirty = False if all_actions and attr_prefix is not None: folder_id = getattr(config, attr_prefix + "_folder_id") action = getattr(config, attr_prefix + "_action").lower() mark_as_read = getattr(config, attr_prefix + "_mark_as_read") if mark_as_read: msg.SetReadState(True) if action == ACTION_NONE: mgr.LogDebug(1, "Not touching message '%s'" % msg.subject) elif action == ACTION_COPY: try: dest_folder = ms.GetFolder(folder_id) except ms.MsgStoreException: print "ERROR: Unable to open the folder to Copy the " \ "message - this message was not copied" else: msg.CopyToReportingError(mgr, dest_folder) mgr.LogDebug(1, "Copied message '%s' to folder '%s'" \ % (msg.subject, dest_folder.GetFQName())) elif action == ACTION_MOVE: try: dest_folder = ms.GetFolder(folder_id) except ms.MsgStoreException: print "ERROR: Unable to open the folder to Move the " \ "message - this message was not moved" else: msg.MoveToReportingError(mgr, dest_folder) mgr.LogDebug(1, "Moved message '%s' to folder '%s'" \ % (msg.subject, dest_folder.GetFQName())) else: raise RuntimeError, "Eeek - bad action '%r'" % (action,) if all_actions: mgr.stats.RecordClassification(prob) mgr.classifier_data.message_db.store_msg(msg) mgr.classifier_data.dirty = True mgr.classifier_data.SavePostIncrementalTrain() return disposition except: print "Failed filtering message!", msg import traceback traceback.print_exc() return "Failed" def filter_folder(f, mgr, config, progress): only_unread = config.only_unread only_unseen = config.only_unseen all_actions = config.action_all dispositions = {} field_name = mgr.config.general.field_score_name for message in f.GetMessageGenerator(): if progress.stop_requested(): break progress.tick() if only_unread and message.GetReadState() or \ only_unseen and message.GetField(field_name) is not None: continue try: disposition = filter_message(message, mgr, all_actions) except: import traceback print "Error filtering message '%s'" % (message,) traceback.print_exc() disposition = "Error" dispositions[disposition] = dispositions.get(disposition, 0) + 1 return dispositions # Called for "filter now" def filterer(mgr, config, progress): config = config.filter_now if not config.folder_ids: progress.error(_("You must specify at least one folder")) return progress.set_status(_("Counting messages")) num_msgs = 0 for f in mgr.message_store.GetFolderGenerator(config.folder_ids, config.include_sub): num_msgs += f.count progress.set_max_ticks(num_msgs+3) dispositions = {} for f in mgr.message_store.GetFolderGenerator(config.folder_ids, config.include_sub): progress.set_status(_("Filtering folder '%s'") % (f.name)) this_dispositions = filter_folder(f, mgr, config, progress) for key, val in this_dispositions.items(): dispositions[key] = dispositions.get(key, 0) + val if progress.stop_requested(): return # All done - report what we did. err_text = "" if dispositions.has_key("Error"): err_text = _(" (%d errors)") % dispositions["Error"] dget = dispositions.get text = _("Found %d spam, %d unsure and %d good messages%s") % \ (dget("Yes",0), dget("Unsure",0), dget("No",0), err_text) progress.set_status(text) def main(): print "Sorry - we don't do anything here any more" if __name__ == "__main__": main() spambayes-1.1a6/Outlook2000/images/0000775000076500000240000000000011355064626017074 5ustar skipstaff00000000000000spambayes-1.1a6/Outlook2000/images/delete_as_spam.bmp0000664000076500000240000000606610646440135022544 0ustar skipstaff00000000000000BM6 6(   @@  @@"$& **"$ **# $# $ "$&)+# $# $ "$&)  "$&)+  $  "$&)  .$      , , "$&##7.$      , , "$##  RI@7    &&"$&  [RI@  $  &&"$ d[RI@7.$  "md[RI@7.  vmd[RI@7        !!vmd[RI@        !!vmd[RI  .$  vmd[R  7.$   55RI@7.$ ,,$$ !! 55[RI@7.$ ,,$$ !!00vmd[RI@7.$    00vmd[RI@7.$   vmd[RI@7.$ **vmd[RI@7.$ **RRvmd[RI@7.$RRvmd[RI@7.$vmd[RI  vmd[R  77  77  spambayes-1.1a6/Outlook2000/images/recover_ham.bmp0000664000076500000240000000606610646440135022071 0ustar skipstaff00000000000000BM6 6(     # $# $# $# $ $ .$ ##7.$ ##  RI@7.$  [RI@7. d[RI.$ md[R7.$ vmd[RI@7.$ vmd[RI@7.$ vmd[RI@7.$ vmd[RI@7.$  vm@7.$!! vI@7.$!!00vmd[@7.$00vmdI@7.vmd[RI@7.$ **vmd[RI@7.$ **vmd[RI@7.$vmd[RI@7.$vmd[RIvmd[R77  77  spambayes-1.1a6/Outlook2000/manager.py0000664000076500000240000012623011116610166017606 0ustar skipstaff00000000000000from __future__ import generators import cPickle import os import sys import errno import types import shutil import traceback import operator import win32api, win32con, win32gui import timer, thread import win32com.client import win32com.client.gencache import pythoncom import msgstore # Characters valid in a filename. Used to nuke bad chars from the profile # name (which we try and use as a filename). # We assume characters > 127 are OK as they may be unicode filename_chars = ('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' '0123456789' """$%'-_@~ `!()^#&+,;=[]""") # Report a message to the user - should only be used for pretty serious errors # hence we also print a traceback. # Module level function so we can report errors creating the manager def _GetParent(): try: return win32gui.GetActiveWindow() except win32gui.error: pass return 0 def _DoMessage(message, title, flags): return win32gui.MessageBox(_GetParent(), message, title, flags) def ReportError(message, title = None): import traceback print "ERROR:", repr(message) if sys.exc_info()[0] is not None: traceback.print_exc() if title is None: title = "SpamBayes" _DoMessage(message, title, win32con.MB_ICONEXCLAMATION) def ReportInformation(message, title = None): if title is None: title = "SpamBayes" _DoMessage(message, title, win32con.MB_ICONINFORMATION) def AskQuestion(message, title = None): if title is None: title = "SpamBayes" return _DoMessage(message, title, win32con.MB_YESNO | \ win32con.MB_ICONQUESTION) == win32con.IDYES # Non-ascii characters in file or directory names only fully work in # Python 2.3.3+, but latin-1 "compatible" filenames should work in 2.3 try: filesystem_encoding = sys.getfilesystemencoding() except AttributeError: filesystem_encoding = "mbcs" # Work out our "application directory", which is # the directory of our main .py/.dll/.exe file we # are running from. if hasattr(sys, "frozen"): assert sys.frozen == "dll", "outlook only supports inproc servers" this_filename = win32api.GetModuleFileName(sys.frozendllhandle) else: this_filename = os.path.abspath(__file__) # Ensure that a bsddb module is available if we are frozen. # See if we can use the new bsddb module. (The old one is unreliable # on Windows, so we don't use that) if hasattr(sys, "frozen"): try: import bsddb3 except ImportError: bsddb3 = None try: import bsddb except ImportError: bsddb = None else: # This name is not in the old (bad) one. if not hasattr(bsddb, "db"): bsddb = None assert bsddb or bsddb3, \ "Don't build binary versions without bsddb!" # This is a little bit of a hack . We are generally in a child # directory of the bayes code. To help installation, we handle the # fact that this may not be on sys.path. Note that doing these # imports is delayed, so that we can set the BAYESCUSTOMIZE envar # first (if we import anything from the core spambayes code before # setting that envar, our .ini file may have no effect). # However, we want *some* Spambayes code before the options are processed # so this is now 2 steps - get the "early" spambayes core stuff (which # must not import spambayes.Options) and sets up sys.path, and "later" core # stuff, which can include spambayes.Options, and assume sys.path in place. def import_early_core_spambayes_stuff(): global bayes_i18n try: from spambayes import OptionsClass except ImportError: parent = os.path.abspath(os.path.join(os.path.dirname(this_filename), "..")) sys.path.insert(0, parent) from spambayes import i18n bayes_i18n = i18n def import_core_spambayes_stuff(ini_filenames): global bayes_classifier, bayes_tokenize, bayes_storage, bayes_options, \ bayes_message, bayes_stats if "spambayes.Options" in sys.modules: # The only thing we are worried about here is spambayes.Options # being imported before we have determined the INI files we need to # use. # The only way this can happen otherwise is when the addin is # de-selected then re-selected via the Outlook GUI - and when # running from source-code, it never appears in this list. # So this should never happen from source-code, and if it does, then # the developer has recently changed something that causes the early # import assert hasattr(sys, "frozen") # And we don't care (we could try and reload the engine options, # but these are very unlikely to have changed) return # ini_filenames may contain Unicode, but environ not unicode aware. # Convert if necessary. use_names = [] for name in ini_filenames: if isinstance(name, unicode): name = name.encode(filesystem_encoding) use_names.append(name) os.environ["BAYESCUSTOMIZE"] = os.pathsep.join(use_names) from spambayes import classifier from spambayes.tokenizer import tokenize from spambayes import storage from spambayes import message from spambayes import Stats bayes_classifier = classifier bayes_tokenize = tokenize bayes_storage = storage bayes_message = message bayes_stats = Stats assert "spambayes.Options" in sys.modules, \ "Expected 'spambayes.Options' to be loaded here" from spambayes.Options import options bayes_options = options # Function to "safely" save a pickle, only overwriting # the existing file after a successful write. def SavePickle(what, filename): temp_filename = filename + ".tmp" file = open(temp_filename,"wb") try: cPickle.dump(what, file, 1) finally: file.close() # now rename to the correct file. try: os.unlink(filename) except os.error: pass os.rename(temp_filename, filename) # Base class for our "storage manager" - we choose between the pickle # and DB versions at runtime. As our bayes uses spambayes.storage, # our base class can share common bayes loading code, and we use # spambayes.message, so the base class can share common message info # code, too. class BasicStorageManager: db_extension = None # for pychecker - overwritten by subclass def __init__(self, bayes_base_name, mdb_base_name): self.bayes_filename = bayes_base_name.encode(filesystem_encoding) + \ self.db_extension self.mdb_filename = mdb_base_name.encode(filesystem_encoding) + \ self.db_extension def new_bayes(self): # Just delete the file and do an "open" try: os.unlink(self.bayes_filename) except EnvironmentError, e: if e.errno != errno.ENOENT: raise return self.open_bayes() def store_bayes(self, bayes): bayes.store() def open_bayes(self): return bayes_storage.open_storage(self.bayes_filename, self.klass) def close_bayes(self, bayes): bayes.close() def open_mdb(self): # MessageInfo storage types may lag behind, so use pickle if the # matching type isn't available. if self.klass in bayes_message._storage_types.keys(): return bayes_message.open_storage(self.mdb_filename, self.klass) return bayes_message.open_storage(self.mdb_filename, "pickle") def store_mdb(self, mdb): mdb.store() def close_mdb(self, mdb): mdb.close() class PickleStorageManager(BasicStorageManager): db_extension = ".pck" klass = "pickle" def new_mdb(self): return {} def is_incremental(self): return False # False means we always save the entire DB class DBStorageManager(BasicStorageManager): db_extension = ".db" klass = "dbm" def new_mdb(self): try: os.unlink(self.mdb_filename) except EnvironmentError, e: if e.errno != errno.ENOENT: raise return self.open_mdb() def is_incremental(self): return True # True means only changed records get actually written class ZODBStorageManager(DBStorageManager): db_extension = ".fs" klass = "zodb" # Encapsulates our entire classification database # This allows a couple of different "databases" to be open at once # eg, a "temporary" one for training, etc. # The manager should contain no database state - it should all be here. class ClassifierData: def __init__(self, db_manager, logger): self.db_manager = db_manager self.bayes = None self.message_db = None self.dirty = False self.logger = logger # currently the manager, but needed only for logging def Load(self): import time start = time.clock() bayes = message_db = None # Exceptions must be caught by caller. # file-not-found handled gracefully by storage. bayes = self.db_manager.open_bayes() fname = self.db_manager.bayes_filename.encode("mbcs", "replace") print "Loaded bayes database from '%s'" % (fname,) message_db = self.db_manager.open_mdb() fname = self.db_manager.mdb_filename.encode("mbcs", "replace") print "Loaded message database from '%s'" % (fname,) self.logger.LogDebug(0, "Bayes database initialized with " "%d spam and %d good messages" % (bayes.nspam, bayes.nham)) # Once, we checked that the message database was the same length # as the training database here. However, we now store information # about messages that are classified but not trained in the message # database, so the lengths will not be equal (unless all messages # are trained). That step doesn't really gain us anything, anyway, # since it no longer would tell us useful information, so remove it. self.bayes = bayes self.message_db = message_db self.dirty = False self.logger.LogDebug(1, "Loaded databases in %gms" % ((time.clock()-start)*1000)) def InitNew(self): if self.bayes is not None: self.db_manager.close_bayes(self.bayes) if self.message_db is not None: self.db_manager.close_mdb(self.message_db) self.bayes = self.db_manager.new_bayes() self.message_db = self.db_manager.new_mdb() self.dirty = True def SavePostIncrementalTrain(self): # Save the database after a training operation - only actually # saves if we aren't using pickles. if self.db_manager.is_incremental(): if self.dirty: self.Save() else: self.logger.LogDebug(1, "Bayes database is not dirty - not writing") else: print "Using a slow database - not saving after incremental train" def Save(self): import time start = time.clock() bayes = self.bayes if self.logger.verbose: print "Saving bayes database with %d spam and %d good messages" %\ (bayes.nspam, bayes.nham) print " ->", self.db_manager.bayes_filename self.db_manager.store_bayes(self.bayes) if self.logger.verbose: print " ->", self.db_manager.mdb_filename self.db_manager.store_mdb(self.message_db) self.dirty = False self.logger.LogDebug(1, "Saved databases in %gms" % ((time.clock()-start)*1000)) def Close(self): if self.dirty and self.bayes: print "Warning: ClassifierData closed while Bayes database dirty" if self.db_manager: self.db_manager.close_bayes(self.bayes) self.db_manager.close_mdb(self.message_db) self.db_manager = None self.bayes = None self.logger = None def Adopt(self, other): assert not other.dirty, "Adopting dirty classifier data!" other.db_manager.close_bayes(other.bayes) other.db_manager.close_mdb(other.message_db) self.db_manager.close_bayes(self.bayes) self.db_manager.close_mdb(self.message_db) # Move the files shutil.move(other.db_manager.bayes_filename, self.db_manager.bayes_filename) shutil.move(other.db_manager.mdb_filename, self.db_manager.mdb_filename) # and re-open. self.Load() def GetStorageManagerClass(): # We used to enforce this so that all binary users used bsddb, and # unless they modified the source, so would all source users. We # would like more flexibility now, so we match what the rest of the # applications do - this isn't exposed via the GUI, so Outlook users # still get bsddb by default, and have to fiddle with a text file # to change that. use_db = bayes_options["Storage", "persistent_use_database"] available = {"pickle" : PickleStorageManager, "dbm" : DBStorageManager, "zodb" : ZODBStorageManager, } if use_db not in available: # User is trying to use something fancy which isn't available. # Fall back on bsddb. print use_db, "storage type not available. Using bsddb." use_db = "dbm" return available[use_db] # Our main "bayes manager" class BayesManager: def __init__(self, config_base="default", outlook=None, verbose=0): self.owner_thread_ident = thread.get_ident() # check we aren't multi-threaded self.never_configured = True self.reported_error_map = {} self.reported_startup_error = False self.config = self.options = None self.addin = None self.verbose = verbose self.outlook = outlook self.dialog_parser = None self.test_suite_running = False self.received_ham = self.received_unsure = self.received_spam = 0 self.notify_timer_id = None import_early_core_spambayes_stuff() self.application_directory = os.path.dirname(this_filename) # Load the environment for translation. lang_manager = bayes_i18n.LanguageManager() # Set the system user default language. lang_manager.set_language(lang_manager.locale_default_lang()) # where windows would like our data stored (and where # we do, unless overwritten via a config file) self.windows_data_directory = self.LocateDataDirectory() # Read the primary configuration files self.PrepareConfig() # See if the initial config files specify a # "data directory". If so, use it, otherwise # use the default Windows data directory for our app. value = self.config.general.data_directory if value: # until I know otherwise, config files are ASCII - but our # file system is unicode to some degree. # (do config files support encodings at all?) # Assume the file system encoding for file names! try: value = value.decode(filesystem_encoding) except AttributeError: # May already be Unicode pass assert isinstance(value, types.UnicodeType), "%r should be a unicode" % value try: if not os.path.isdir(value): os.makedirs(value) assert os.path.isdir(value), "just made the *ucker" value = os.path.abspath(value) except os.error: print "The configuration files have specified a data " \ "directory of", repr(value), "but it is not valid. " \ "Using default." value = None if value: self.data_directory = value else: self.data_directory = self.windows_data_directory # Get the message store before loading config, as we use the profile # name. self.message_store = msgstore.MAPIMsgStore(outlook) self.LoadConfig() # Load the options for the classifier. We support # default_bayes_customize.ini in the app directory and user data # directory (version 0.8 and earlier, we copied the app one to the # user dir - that was a mistake - but supporting a version in that # directory wasn't). We also look for a # {outlook-profile-name}_bayes_customize.ini file in the data # directory, to allow per-profile customisations. bayes_option_filenames = [] # data dir last so options there win. for look_dir in [self.application_directory, self.data_directory]: look_file = os.path.join(look_dir, "default_bayes_customize.ini") if os.path.isfile(look_file): bayes_option_filenames.append(look_file) look_file = os.path.join(self.data_directory, self.GetProfileName() + \ "_bayes_customize.ini") if os.path.isfile(look_file): bayes_option_filenames.append(look_file) import_core_spambayes_stuff(bayes_option_filenames) # Set interface to use the user language in configuration file. for language in bayes_options["globals", "language"][::-1]: # We leave the default in there as the last option, to fall # back on if necessary. lang_manager.add_language(language) self.LogDebug(1, "Asked to add languages: " + \ ", ".join(bayes_options["globals", "language"])) self.LogDebug(1, "Set language to " + \ str(lang_manager.current_langs_codes)) bayes_base = os.path.join(self.data_directory, "default_bayes_database") mdb_base = os.path.join(self.data_directory, "default_message_database") # determine which db manager to use, and create it. ManagerClass = GetStorageManagerClass() db_manager = ManagerClass(bayes_base, mdb_base) self.classifier_data = ClassifierData(db_manager, self) try: self.classifier_data.Load() except: self.ReportFatalStartupError("Failed to load bayes database") self.classifier_data.InitNew() self.bayes_options = bayes_options self.bayes_message = bayes_message bayes_options["Categorization", "spam_cutoff"] = \ self.config.filter.spam_threshold \ / 100.0 bayes_options["Categorization", "ham_cutoff"] = \ self.config.filter.unsure_threshold \ / 100.0 self.stats = bayes_stats.Stats(bayes_options, self.classifier_data.message_db) def AdoptClassifierData(self, new_classifier_data): self.classifier_data.Adopt(new_classifier_data) self.stats.messageinfo_db = self.classifier_data.message_db # Logging - this should be somewhere else. def LogDebug(self, level, *args): if self.verbose >= level: for arg in args[:-1]: print arg, print args[-1] def ReportError(self, message, title = None): if self.test_suite_running: print "ReportError:", repr(message) print "(but test suite running - not reported)" return ReportError(message, title) def ReportInformation(self, message, title=None): if self.test_suite_running: print "ReportInformation:", repr(message) print "(but test suite running - not reported)" return ReportInformation(message, title) def AskQuestion(self, message, title=None): return AskQuestion(message, title) # Report a super-serious startup error to the user. # This should only be used when SpamBayes was previously working, but a # critical error means we are probably not working now. # We just report the first such error - subsequent ones are likely a result of # the first - hence, this must only be used for startup errors. def ReportFatalStartupError(self, message): if not self.reported_startup_error: self.reported_startup_error = True full_message = _(\ "There was an error initializing the Spam plugin.\r\n\r\n" \ "Spam filtering has been disabled. Please re-configure\r\n" \ "and re-enable this plugin\r\n\r\n" \ "Error details:\r\n") + message # Disable the plugin if self.config is not None: self.config.filter.enabled = False self.ReportError(full_message) else: # We have reported the error, but for the sake of the log, we # still want it logged there. print "ERROR:", repr(message) traceback.print_exc() def ReportErrorOnce(self, msg, title = None, key = None): if key is None: key = msg # Always print the message and traceback. if self.test_suite_running: print "ReportErrorOnce:", repr(msg) print "(but test suite running - not reported)" return print "ERROR:", repr(msg) if key in self.reported_error_map: print "(this error has already been reported - not displaying it again)" else: traceback.print_exc() self.reported_error_map[key] = True ReportError(msg, title) # Outlook used to give us thread grief - now we avoid Outlook # from threads, but this remains a worthwhile abstraction. def WorkerThreadStarting(self): pythoncom.CoInitialize() def WorkerThreadEnding(self): pythoncom.CoUninitialize() def LocateDataDirectory(self): # Locate the best directory for our data files. from win32com.shell import shell, shellcon try: appdata = shell.SHGetFolderPath(0,shellcon.CSIDL_APPDATA,0,0) path = os.path.join(appdata, "SpamBayes") if not os.path.isdir(path): os.makedirs(path) return path except pythoncom.com_error: # Function doesn't exist on early win95, # and it may just fail anyway! return self.application_directory except EnvironmentError: # Can't make the directory. return self.application_directory def FormatFolderNames(self, folder_ids, include_sub): names = [] for eid in folder_ids: try: folder = self.message_store.GetFolder(eid) name = folder.name except self.message_store.MsgStoreException: name = "" names.append(name) ret = '; '.join(names) if include_sub: ret += " (incl. Sub-folders)" return ret def EnsureOutlookFieldsForFolder(self, folder_id, include_sub=False): # Should be called at least once once per folder you are # watching/filtering etc # Ensure that our fields exist on the Outlook *folder* # Setting properties via our msgstore (via Ext Mapi) sets the props # on the message OK, but Outlook doesn't see it as a "UserProperty". # Using MAPI to set them directly on the folder also has no effect. # Later: We have since discovered that Outlook stores user property # information in the 'associated contents' folder - see # msgstore.MAPIMsgStoreFolder.DoesFolderHaveOutlookField() for more # details. We can reverse engineer this well enough to determine # if a property exists, but not well enough to actually add a # property. Thus, we resort to the Outlook object model to actually # add it. # Note that this means we need an object in the folder to modify. # We could go searching for an existing item then modify and save it # (indeed, we did once), but this could be bad-form, as the message # we randomly choose to modify will then have a meaningless 'Spam' # field. If we are going to go to the effort of creating a temp # item when no item exists, we may as well do it all the time, # especially now we know how to check if the folder has the field # without opening an Outlook item. # Regarding the property type: # We originally wanted to use the "Integer" Outlook field, # but it seems this property type alone is not exposed via the Object # model. So we resort to olPercent, and live with the % sign # (which really is OK!) assert self.outlook is not None, "I need outlook :(" field_name = self.config.general.field_score_name for msgstore_folder in self.message_store.GetFolderGenerator( [folder_id], include_sub): folder_name = msgstore_folder.GetFQName() if msgstore_folder.DoesFolderHaveOutlookField(field_name): self.LogDebug(1, "Folder '%s' already has field '%s'" \ % (folder_name, field_name)) continue self.LogDebug(0, "Folder '%s' has no field named '%s' - creating" \ % (folder_name, field_name)) # Creating the item via the Outlook model does some strange # things (such as moving it to "Drafts" on save), so we create # it using extended MAPI (via our msgstore) message = msgstore_folder.CreateTemporaryMessage(msg_flags=1) outlook_message = message.GetOutlookItem() ups = outlook_message.UserProperties try: # Display format is documented as being the 1-based index in # the combo box in the outlook UI for the given data type. # 1 is the first - "Rounded", which seems fine. format = 1 ups.Add(field_name, win32com.client.constants.olPercent, True, # Add to folder format) outlook_message.Save() except pythoncom.com_error, details: if msgstore.IsReadOnlyCOMException(details): self.LogDebug(1, "The folder '%s' is read-only - user " "property can't be added" % (folder_name,)) else: print "Warning: failed to create the Outlook " \ "user-property in folder '%s'" \ % (folder_name,) print "", details msgstore_folder.DeleteMessages((message,)) # Check our DoesFolderHaveOutlookField logic holds up. if not msgstore_folder.DoesFolderHaveOutlookField(field_name): self.LogDebug(0, "WARNING: We just created the user field in folder " "%s, but it appears to not exist. Something is " "probably wrong with DoesFolderHaveOutlookField()" % \ folder_name) def PrepareConfig(self): # Load our Outlook specific configuration. This is done before # SpamBayes is imported, and thus we are able to change the INI # file used for the engine. It is also done before the primary # options are loaded - this means we can change the directory # from which these options are loaded. import config self.options = config.CreateConfig() # Note that self.options really *is* self.config - but self.config # allows a "." notation to access the values. Changing one is reflected # immediately in the other. self.config = config.OptionsContainer(self.options) filename = os.path.join(self.application_directory, "default_configuration.ini") self._MergeConfigFile(filename) filename = os.path.join(self.windows_data_directory, "default_configuration.ini") self._MergeConfigFile(filename) def _MergeConfigFile(self, filename): try: self.options.merge_file(filename) except: msg = _("The configuration file named below is invalid.\r\n" \ "Please either correct or remove this file\r\n\r\n" \ "Filename: ") + filename self.ReportError(msg) def GetProfileName(self): profile_name = self.message_store.GetProfileName() # The profile name may include characters invalid in file names. if profile_name is not None: profile_name = "".join([c for c in profile_name if ord(c)>127 or c in filename_chars]) if profile_name is None: # should only happen in source-code versions - older win32alls can't # determine this. profile_name = "unknown_profile" print "*** NOTE: It appears you are running the source-code version of" print "* SpamBayes, and running a win32all version pre 154." print "* If you work with multiple Outlook profiles, it is recommended" print "* you upgrade - see http://starship.python.net/crew/mhammond""" return profile_name def LoadConfig(self): # Insist on English numeric conventions in config file. # See addin.py, and [725466] Include a proper locale fix in Options.py import locale; locale.setlocale(locale.LC_NUMERIC, "C") profile_name = self.GetProfileName() self.config_filename = os.path.join(self.data_directory, profile_name + ".ini") self.never_configured = not os.path.exists(self.config_filename) # Now load it up self._MergeConfigFile(self.config_filename) # Set global verbosity from the options file. self.verbose = self.config.general.verbose if self.verbose: self.LogDebug(self.verbose, "System verbosity set to", self.verbose) # Do any migrations - first the old pickle into the new format. self.MigrateOldPickle() # Then any options we change (particularly any 'experimental' ones we # consider important) import config config.MigrateOptions(self.options) if self.verbose > 1: print "Dumping loaded configuration:" print self.options.display() print "-- end of configuration --" def MigrateOldPickle(self): assert self.config is not None, "Must have a config" pickle_filename = os.path.join(self.data_directory, "default_configuration.pck") try: f = open(pickle_filename, 'rb') except IOError: self.LogDebug(1, "No old pickle file to migrate") return print "Migrating old pickle '%s'" % pickle_filename try: try: old_config = cPickle.load(f) except: print "FAILED to load old pickle" traceback.print_exc() msg = _("There was an error loading your old\r\n" \ "SpamBayes configuration file.\r\n\r\n" \ "It is likely that you will need to re-configure\r\n" \ "SpamBayes before it will function correctly.") self.ReportError(msg) # But we can't abort yet - we really should still try and # delete it, as we aren't gunna work next time in this case! old_config = None finally: f.close() if old_config is not None: for section, items in old_config.__dict__.items(): print " migrating section '%s'" % (section,) # exactly one value wasn't in a section - now in "general" dict = getattr(items, "__dict__", None) if dict is None: dict = {section: items} section = "general" for name, value in dict.items(): sect = getattr(self.config, section) setattr(sect, name, value) # Save the config, then delete the pickle so future attempts to # migrate will fail. We save first, so failure here means next # attempt should still find the pickle. self.LogDebug(1, "pickle migration doing initial configuration save") try: self.LogDebug(1, "pickle migration removing '%s'" % pickle_filename) os.remove(pickle_filename) except os.error: msg = _("There was an error migrating and removing your old\r\n" \ "SpamBayes configuration file. Configuration changes\r\n" \ "you make are unlikely to be reflected next\r\n" \ "time you start Outlook. Please try rebooting.") self.ReportError(msg) def GetClassifier(self): """Return the classifier we're using.""" return self.classifier_data.bayes def SaveConfig(self): # Insist on english numeric conventions in config file. # See addin.py, and [725466] Include a proper locale fix in Options.py import locale; locale.setlocale(locale.LC_NUMERIC, "C") # Update our runtime verbosity from the options. self.verbose = self.config.general.verbose print "Saving configuration ->", self.config_filename.encode("mbcs", "replace") assert self.config and self.options, "Have no config to save!" if self.verbose > 1: print "Dumping configuration to save:" print self.options.display() print "-- end of configuration --" self.options.update_file(self.config_filename) def Save(self): # No longer save the config here - do it explicitly when changing it # (prevents lots of extra pickle writes, for no good reason. Other # alternative is a dirty flag for config - this is simpler) if self.classifier_data.dirty: self.classifier_data.Save() else: self.LogDebug(1, "Bayes database is not dirty - not writing") def Close(self): global _mgr self._KillNotifyTimer() self.classifier_data.Close() self.config = self.options = None if self.message_store is not None: self.message_store.Close() self.message_store = None self.outlook = None self.addin = None # If we are the global manager, reset that if _mgr is self: _mgr = None def score(self, msg, evidence=False): """Score a msg. If optional arg evidence is specified and true, the result is a two-tuple score, clues where clues is a list of the (word, spamprob(word)) pairs that went into determining the score. Else just the score is returned. """ email = msg.GetEmailPackageObject() try: return self.classifier_data.bayes.spamprob(bayes_tokenize(email), evidence) except AssertionError: # See bug 706520 assert fails in classifier # For now, just tell the user. msg = _("It appears your SpamBayes training database is corrupt.\r\n\r\n" \ "We are working on solving this, but unfortunately you\r\n" \ "must re-train the system via the SpamBayes manager.") self.ReportErrorOnce(msg) # and disable the addin, as we are hosed! self.config.filter.enabled = False raise def GetDisabledReason(self): # Gets the reason why the plugin can not be enabled. # If return is None, then it can be enabled (and indeed may be!) # Otherwise return is the string reason config = self.config.filter ok_to_enable = operator.truth(config.watch_folder_ids) if not ok_to_enable: return _("You must define folders to watch for new messages. " \ "Select the 'Filtering' tab to define these folders.") ok_to_enable = operator.truth(config.spam_folder_id) if not ok_to_enable: return _("You must define the folder to receive your certain spam. " \ "Select the 'Filtering' tab to define this folder.") # Check that the user hasn't selected the same folder as both # 'Spam' or 'Unsure', and 'Watch' - this would confuse us greatly. ms = self.message_store unsure_folder = None # unsure need not be specified. if config.unsure_folder_id: try: unsure_folder = ms.GetFolder(config.unsure_folder_id) except ms.MsgStoreException, details: return _("The unsure folder is invalid: %s") % (details,) try: spam_folder = ms.GetFolder(config.spam_folder_id) except ms.MsgStoreException, details: return _("The spam folder is invalid: %s") % (details,) if ok_to_enable: for folder in ms.GetFolderGenerator(config.watch_folder_ids, config.watch_include_sub): bad_folder_type = None if unsure_folder is not None and unsure_folder == folder: bad_folder_type = _("unsure") bad_folder_name = unsure_folder.GetFQName() if spam_folder == folder: bad_folder_type = _("spam") bad_folder_name = spam_folder.GetFQName() if bad_folder_type is not None: return _("You can not specify folder '%s' as both the " \ "%s folder, and as being watched.") \ % (bad_folder_name, bad_folder_type) return None def ShowManager(self): import dialogs dialogs.ShowDialog(0, self, self.config, "IDD_MANAGER") # And re-save now, just incase Outlook dies on the way down. self.SaveConfig() # And update the cutoff values in bayes_options (which the # stats use) to our thresholds. bayes_options["Categorization", "spam_cutoff"] = \ self.config.filter.spam_threshold \ / 100.0 bayes_options["Categorization", "ham_cutoff"] = \ self.config.filter.unsure_threshold \ / 100.0 # And tell the addin that our filters may have changed. if self.addin is not None: self.addin.FiltersChanged() def ShowFilterNow(self): import dialogs dialogs.ShowDialog(0, self, self.config, "IDD_FILTER_NOW") # And re-save now, just incase Outlook dies on the way down. self.SaveConfig() def ShowHtml(self,url): """Displays the main SpamBayes documentation in your Web browser""" import sys, os, urllib if urllib.splittype(url)[0] is None: # just a file spec if hasattr(sys, "frozen"): # New binary is in ../docs/outlook relative to executable. fname = os.path.join(os.path.dirname(sys.argv[0]), "../docs/outlook", url) if not os.path.isfile(fname): # Still support same directory as to the executable. fname = os.path.join(os.path.dirname(sys.argv[0]), url) else: # (ie, main Outlook2000) dir fname = os.path.join(os.path.dirname(__file__), url) fname = os.path.abspath(fname) if not os.path.isfile(fname): self.ReportError("Can't find "+url) return url = fname # else assume it is valid! from dialogs import SetWaitCursor SetWaitCursor(1) os.startfile(url) SetWaitCursor(0) def HandleNotification(self, disposition): if self.config.notification.notify_sound_enabled: if disposition == "Yes": self.received_spam += 1 elif disposition == "No": self.received_ham += 1 else: self.received_unsure += 1 self._StartNotifyTimer() def _StartNotifyTimer(self): # First kill any existing timer self._KillNotifyTimer() # And start a new timer. delay = self.config.notification.notify_accumulate_delay self._DoStartNotifyTimer(delay) def _DoStartNotifyTimer(self, delay): assert thread.get_ident() == self.owner_thread_ident assert self.notify_timer_id is None, "Shouldn't start a timer when already have one" assert isinstance(delay, types.FloatType), "Timer values are float seconds" # And start a new timer. assert delay, "No delay means no timer!" delay = int(delay*1000) # convert to ms. self.notify_timer_id = timer.set_timer(delay, self._NotifyTimerFunc) self.LogDebug(1, "Notify timer started - id=%d, delay=%d" % (self.notify_timer_id, delay)) def _KillNotifyTimer(self): assert thread.get_ident() == self.owner_thread_ident if self.notify_timer_id is not None: timer.kill_timer(self.notify_timer_id) self.LogDebug(2, "The notify timer with id=%d was stopped" % self.notify_timer_id) self.notify_timer_id = None def _NotifyTimerFunc(self, event, time): # Kill the timer first assert thread.get_ident() == self.owner_thread_ident self.LogDebug(1, "The notify timer with id=%s fired" % self.notify_timer_id) self._KillNotifyTimer() import winsound config = self.config.notification sound_opts = winsound.SND_FILENAME | winsound.SND_ASYNC | winsound.SND_NOSTOP | winsound.SND_NODEFAULT self.LogDebug(3, "Notify received ham=%d, unsure=%d, spam=%d" % (self.received_ham, self.received_unsure, self.received_spam)) if self.received_ham > 0 and len(config.notify_ham_sound) > 0: self.LogDebug(3, "Playing ham sound '%s'" % config.notify_ham_sound) winsound.PlaySound(config.notify_ham_sound, sound_opts) elif self.received_unsure > 0 and len(config.notify_unsure_sound) > 0: self.LogDebug(3, "Playing unsure sound '%s'" % config.notify_unsure_sound) winsound.PlaySound(config.notify_unsure_sound, sound_opts) elif self.received_spam > 0 and len(config.notify_spam_sound) > 0: self.LogDebug(3, "Playing spam sound '%s'" % config.notify_spam_sound) winsound.PlaySound(config.notify_spam_sound, sound_opts) # Reset received counts to zero after notify. self.received_ham = self.received_unsure = self.received_spam = 0 _mgr = None def GetManager(outlook = None): global _mgr if _mgr is None: if outlook is None: outlook = win32com.client.Dispatch("Outlook.Application") _mgr = BayesManager(outlook=outlook) return _mgr def ShowManager(mgr): mgr.ShowManager() def main(verbose_level = 1): mgr = GetManager() mgr.verbose = max(mgr.verbose, verbose_level) ShowManager(mgr) mgr.Save() mgr.Close() return 0 def usage(): print "Usage: manager [-v ...]" sys.exit(1) if __name__=='__main__': verbose = 1 import getopt opts, args = getopt.getopt(sys.argv[1:], "v") if args: usage() for opt, val in opts: if opt=="-v": verbose += 1 else: usage() sys.exit(main(verbose)) spambayes-1.1a6/Outlook2000/msgstore.py0000664000076500000240000021407111116610264020037 0ustar skipstaff00000000000000from __future__ import generators import sys, os, re import locale from time import timezone import email from email.MIMEImage import MIMEImage from email.Message import Message from email.MIMEMultipart import MIMEMultipart from email.MIMEText import MIMEText from email.Parser import HeaderParser from email.Utils import formatdate try: from cStringIO import StringIO except ImportError: from StringIO import StringIO # MAPI imports etc. from win32com.client import Dispatch, constants from win32com.mapi import mapi, mapiutil from win32com.mapi.mapitags import * import pythoncom import winerror # Additional MAPI constants we dont have in Python MESSAGE_MOVE = 0x1 # from MAPIdefs.h MSGFLAG_READ = 0x1 # from MAPIdefs.h MSGFLAG_UNSENT = 0x00000008 MYPR_BODY_HTML_A = 0x1013001e # magic MYPR_BODY_HTML_W = 0x1013001f # ditto MYPR_MESSAGE_ID_A = 0x1035001E # more magic (message id field used for Exchange) CLEAR_READ_FLAG = 0x00000004 CLEAR_RN_PENDING = 0x00000020 CLEAR_NRN_PENDING = 0x00000040 SUPPRESS_RECEIPT = 0x1 FOLDER_DIALOG = 0x00000002 USE_DEFERRED_ERRORS = mapi.MAPI_DEFERRED_ERRORS # or set to zero to see what changes #import warnings #if sys.version_info >= (2, 3): # # sick off the new hex() warnings! # warnings.filterwarnings("ignore", category=FutureWarning, append=1) # Nod to our automated test suite. Currently supports a hack so our test # message is filtered, and also for raising exceptions at key times. # see tester.py for more details. test_suite_running = False test_suite_failure_request = None test_suite_failure = None # Set to the number of times we should fail, or None for all times. test_suite_failure_count = None # Sometimes the test suite will request that we simulate MAPI errors. def help_test_suite(checkpoint_name): global test_suite_failure_request, test_suite_failure_count if test_suite_running and \ test_suite_failure_request == checkpoint_name: if test_suite_failure_count: test_suite_failure_count -= 1 if test_suite_failure_count==0: test_suite_failure_request = None raise test_suite_failure[0], test_suite_failure[1] # Exceptions raised by this module. Raw MAPI exceptions should never # be raised to the caller. class MsgStoreException(Exception): def __init__(self, mapi_exception, extra_msg = None): self.mapi_exception = mapi_exception self.extra_msg = extra_msg Exception.__init__(self, mapi_exception, extra_msg) def __str__(self): try: if self.mapi_exception is not None: err_str = GetCOMExceptionString(self.mapi_exception) else: err_str = self.extra_msg or '' return "%s: %s" % (self.__class__.__name__, err_str) # Python silently consumes exceptions here, and uses # except: print "FAILED to str() a MsgStore exception!" import traceback traceback.print_exc() # Exception raised when you attempt to get a message or folder that doesn't # exist. Usually means you are querying an ID that *was* valid, but has # since been moved or deleted. # Note you may get this exception "getting" objects (such as messages or # folders), or accessing properties once the object was created (the message # may be moved under us at any time) class NotFoundException(MsgStoreException): pass # Exception raised when you try and modify a "read only" object. # Only currently examples are Hotmail and IMAP folders. class ReadOnlyException(MsgStoreException): pass # The object has changed since it was opened. class ObjectChangedException(MsgStoreException): pass # Utility functions for exceptions. Convert a COM exception to the best # manager exception. def MsgStoreExceptionFromCOMException(com_exc): if IsNotFoundCOMException(com_exc): return NotFoundException(com_exc) if IsReadOnlyCOMException(com_exc): return ReadOnlyException(com_exc) scode = NormalizeCOMException(com_exc)[0] # And simple scode based ones. if scode == mapi.MAPI_E_OBJECT_CHANGED: return ObjectChangedException(com_exc) return MsgStoreException(com_exc) def NormalizeCOMException(exc_val): hr, msg, exc, arg_err = exc_val if hr == winerror.DISP_E_EXCEPTION and exc: # 'client' exception - unpack 'exception object' wcode, source, msg, help1, help2, hr = exc return hr, msg, exc, arg_err # Build a reasonable string from a COM exception tuple def GetCOMExceptionString(exc_val): hr, msg, exc, arg_err = NormalizeCOMException(exc_val) err_string = mapiutil.GetScodeString(hr) return "Exception 0x%x (%s): %s" % (hr, err_string, msg) # Does this exception probably mean "object not found"? def IsNotFoundCOMException(exc_val): hr, msg, exc, arg_err = NormalizeCOMException(exc_val) return hr in [mapi.MAPI_E_OBJECT_DELETED, mapi.MAPI_E_NOT_FOUND] # Does this exception probably mean "object not available 'cos you ain't logged # in, or 'cos the server is down"? def IsNotAvailableCOMException(exc_val): hr, msg, exc, arg_err = NormalizeCOMException(exc_val) return hr == mapi.MAPI_E_FAILONEPROVIDER def IsReadOnlyCOMException(exc_val): # This seems to happen for IMAP mails (0x800cccd3) # and also for hotmail messages (0x8004dff7) known_failure_codes = -2146644781, -2147164169 exc_val = NormalizeCOMException(exc_val) return exc_val[0] in known_failure_codes def ReportMAPIError(manager, what, exc_val): hr, exc_msg, exc, arg_err = exc_val if hr == mapi.MAPI_E_TABLE_TOO_BIG: err_msg = what + _(" failed as one of your\r\n" \ "Outlook folders is full. Futher operations are\r\n" \ "likely to fail until you clean up this folder.\r\n\r\n" \ "This message will not be reported again until SpamBayes\r\n"\ "is restarted.") else: err_msg = what + _(" failed due to an unexpected Outlook error.\r\n") \ + GetCOMExceptionString(exc_val) + "\r\n\r\n" + \ _("It is recommended you restart Outlook at the earliest opportunity\r\n\r\n" \ "This message will not be reported again until SpamBayes\r\n"\ "is restarted.") manager.ReportErrorOnce(err_msg) # Our objects. class MAPIMsgStore: # Stash exceptions in the class for ease of use by consumers. MsgStoreException = MsgStoreException NotFoundException = NotFoundException ReadOnlyException = ReadOnlyException ObjectChangedException = ObjectChangedException def __init__(self, outlook = None): self.outlook = outlook cwd = os.getcwd() # remember the cwd - mapi changes it under us! mapi.MAPIInitialize(None) logonFlags = (mapi.MAPI_NO_MAIL | mapi.MAPI_EXTENDED | mapi.MAPI_USE_DEFAULT) self.session = mapi.MAPILogonEx(0, None, None, logonFlags) # Note that if the CRT still has a default "C" locale, MAPILogonEx() # will change it. See locale comments in addin.py locale.setlocale(locale.LC_NUMERIC, "C") self.mapi_msg_stores = {} self.default_store_bin_eid = None os.chdir(cwd) def Close(self): self.mapi_msg_stores = None self.session.Logoff(0, 0, 0) self.session = None mapi.MAPIUninitialize() def GetProfileName(self): # Return the name of the MAPI profile currently in use. # XXX - note - early win32all versions are missing # GetStatusTable :( try: self.session.GetStatusTable except AttributeError: # We try and recover from this when win32all is updated, so no need to whinge. return None MAPI_SUBSYSTEM = 39 restriction = mapi.RES_PROPERTY, (mapi.RELOP_EQ, PR_RESOURCE_TYPE, (PR_RESOURCE_TYPE, MAPI_SUBSYSTEM)) table = self.session.GetStatusTable(0) rows = mapi.HrQueryAllRows(table, (PR_DISPLAY_NAME_A,), # columns to retrieve restriction, # only these rows None, # any sort order is fine 0) # any # of results is fine assert len(rows)==1, "Should be exactly one row" (tag, val), = rows[0] # I can't convince MAPI to give me the Unicode name, so we assume # encoded as MBCS. return val.decode("mbcs", "ignore") def _GetMessageStore(self, store_eid): # bin eid. try: # Will usually be pre-fetched, so fast-path out return self.mapi_msg_stores[store_eid] except KeyError: pass given_store_eid = store_eid if store_eid is None: # Find the EID for the default store. tab = self.session.GetMsgStoresTable(0) # Restriction for the table: get rows where PR_DEFAULT_STORE is true. # There should be only one. restriction = (mapi.RES_PROPERTY, # a property restriction (mapi.RELOP_EQ, # check for equality PR_DEFAULT_STORE, # of the PR_DEFAULT_STORE prop (PR_DEFAULT_STORE, True))) # with True rows = mapi.HrQueryAllRows(tab, (PR_ENTRYID,), # columns to retrieve restriction, # only these rows None, # any sort order is fine 0) # any # of results is fine # get first entry, a (property_tag, value) pair, for PR_ENTRYID row = rows[0] eid_tag, store_eid = row[0] self.default_store_bin_eid = store_eid # Open it. store = self.session.OpenMsgStore( 0, # no parent window store_eid, # msg store to open None, # IID; accept default IMsgStore # need write access to add score fields mapi.MDB_WRITE | # we won't send or receive email mapi.MDB_NO_MAIL | USE_DEFERRED_ERRORS) # cache it self.mapi_msg_stores[store_eid] = store if given_store_eid is None: # The default store self.mapi_msg_stores[None] = store return store def GetRootFolder(self, store_id = None): # if storeID is None, gets the root folder from the default store. store = self._GetMessageStore(store_id) hr, data = store.GetProps((PR_ENTRYID, PR_IPM_SUBTREE_ENTRYID), 0) store_eid = data[0][1] subtree_eid = data[1][1] eid = mapi.HexFromBin(store_eid), mapi.HexFromBin(subtree_eid) return self.GetFolder(eid) def _OpenEntry(self, id, iid = None, flags = None): # id is already normalized. store_id, item_id = id store = self._GetMessageStore(store_id) if flags is None: flags = mapi.MAPI_MODIFY | USE_DEFERRED_ERRORS return store.OpenEntry(item_id, iid, flags) # Normalize an "external" hex ID to an internal binary ID. def NormalizeID(self, item_id): assert type(item_id)==type(()), \ "Item IDs must be a tuple (not a %r)" % item_id try: store_id, entry_id = item_id return mapi.BinFromHex(store_id), mapi.BinFromHex(entry_id) except ValueError: raise MsgStoreException(None, "The specified ID '%s' is invalid" % (item_id,)) def _GetSubFolderIter(self, folder): table = folder.GetHierarchyTable(0) rows = mapi.HrQueryAllRows(table, (PR_ENTRYID, PR_STORE_ENTRYID, PR_DISPLAY_NAME_A), None, None, 0) for (eid_tag, eid), (store_eid_tag, store_eid), (name_tag, name) in rows: item_id = store_eid, eid sub = self._OpenEntry(item_id) table = sub.GetContentsTable(0) yield MAPIMsgStoreFolder(self, item_id, name, table.GetRowCount(0)) for store_folder in self._GetSubFolderIter(sub): yield store_folder def GetFolderGenerator(self, folder_ids, include_sub): for folder_id in folder_ids: try: folder_id = self.NormalizeID(folder_id) except MsgStoreException, details: print "NOTE: Skipping invalid folder", details continue try: folder = self._OpenEntry(folder_id) table = folder.GetContentsTable(0) except pythoncom.com_error, details: # We will ignore *all* such errors for the time # being, but give verbose details for results we don't # know about if IsNotAvailableCOMException(details): print "NOTE: Skipping folder for this session - temporarily unavailable" elif IsNotFoundCOMException(details): print "NOTE: Skipping deleted folder" else: print "WARNING: Unexpected MAPI error opening folder" print GetCOMExceptionString(details) continue rc, props = folder.GetProps( (PR_DISPLAY_NAME_A,), 0) yield MAPIMsgStoreFolder(self, folder_id, props[0][1], table.GetRowCount(0)) if include_sub: for f in self._GetSubFolderIter(folder): yield f def GetFolder(self, folder_id): # Return a single folder given the ID. try: # catch all MAPI errors try: # See if this is an Outlook folder item sid = mapi.BinFromHex(folder_id.StoreID) eid = mapi.BinFromHex(folder_id.EntryID) folder_id = sid, eid except AttributeError: # No 'EntryID'/'StoreID' properties - a 'normal' ID folder_id = self.NormalizeID(folder_id) folder = self._OpenEntry(folder_id) table = folder.GetContentsTable(0) # Ensure we have a long-term ID. rc, props = folder.GetProps( (PR_ENTRYID, PR_DISPLAY_NAME_A), 0) folder_id = folder_id[0], props[0][1] return MAPIMsgStoreFolder(self, folder_id, props[1][1], table.GetRowCount(0)) except pythoncom.com_error, details: raise MsgStoreExceptionFromCOMException(details) def GetMessage(self, message_id): # Return a single message given either the ID, or an Outlook # message representing the object. try: # catch all MAPI exceptions. try: eid = mapi.BinFromHex(message_id.EntryID) sid = mapi.BinFromHex(message_id.Parent.StoreID) message_id = sid, eid except AttributeError: # No 'EntryID'/'StoreID' properties - a 'normal' ID message_id = self.NormalizeID(message_id) mapi_object = self._OpenEntry(message_id) hr, data = mapi_object.GetProps(MAPIMsgStoreMsg.message_init_props,0) return MAPIMsgStoreMsg(self, data) except pythoncom.com_error, details: raise MsgStoreExceptionFromCOMException(details) def YieldReceiveFolders(self, msg_class = "IPM.Note"): # Get the main receive folder for each message store. tab = self.session.GetMsgStoresTable(0) rows = mapi.HrQueryAllRows(tab, (PR_ENTRYID,), # columns to retrieve None, # all rows None, # any sort order is fine 0) # any # of results is fine for row in rows: # get first entry, a (property_tag, value) pair, for PR_ENTRYID eid_tag, store_eid = row[0] try: store = self._GetMessageStore(store_eid) folder_eid, ret_class = store.GetReceiveFolder(msg_class, 0) hex_folder_eid = mapi.HexFromBin(folder_eid) hex_store_eid = mapi.HexFromBin(store_eid) except pythoncom.com_error, details: if not IsNotAvailableCOMException(details): print "ERROR enumerating a receive folder -", details continue try: folder = self.GetFolder((hex_store_eid, hex_folder_eid)) # For 'unconfigured' stores, or "stand-alone" PST files, # this is a root folder - so not what we wan't. Only return # folders with a parent. if folder.GetParent() is not None: yield folder except MsgStoreException, details: print "ERROR opening receive folder -", details # but we just continue continue _MapiTypeMap = { type(0.0): PT_DOUBLE, type(0): PT_I4, type(''): PT_STRING8, type(u''): PT_UNICODE, # In Python 2.2.2, bool isn't a distinct type (type(1==1) is type(0)). # type(1==1): PT_BOOLEAN, } def GetPropFromStream(mapi_object, prop_id): try: stream = mapi_object.OpenProperty(prop_id, pythoncom.IID_IStream, 0, 0) chunks = [] while 1: chunk = stream.Read(4096) if not chunk: break chunks.append(chunk) return "".join(chunks) except pythoncom.com_error, d: print "Error getting property", mapiutil.GetPropTagName(prop_id), \ "from stream:", d return "" def GetPotentiallyLargeStringProp(mapi_object, prop_id, row): got_tag, got_val = row if PROP_TYPE(got_tag) == PT_ERROR: ret = "" if got_val == mapi.MAPI_E_NOT_FOUND: pass # No property for this message. elif got_val == mapi.MAPI_E_NOT_ENOUGH_MEMORY: # Too big for simple properties - get via a stream ret = GetPropFromStream(mapi_object, prop_id) else: tag_name = mapiutil.GetPropTagName(prop_id) err_string = mapiutil.GetScodeString(got_val) print "Warning - failed to get property %s: %s" % (tag_name, err_string) else: ret = got_val return ret # Some nasty stuff for getting RTF out of the message def GetHTMLFromRTFProperty(mapi_object, prop_tag = PR_RTF_COMPRESSED): try: rtf_stream = mapi_object.OpenProperty(prop_tag, pythoncom.IID_IStream, 0, 0) html_stream = mapi.WrapCompressedRTFStream(rtf_stream, 0) html = mapi.RTFStreamToHTML(html_stream) except pythoncom.com_error, details: if not IsNotFoundCOMException(details): print "ERROR getting RTF body", details return "" # html may be None if RTF not originally from HTML, but here we # always want a string return html or '' class MAPIMsgStoreFolder: def __init__(self, msgstore, id, name, count): self.msgstore = msgstore self.id = id self.name = name self.count = count def __repr__(self): return "<%s '%s' (%d items), id=%s/%s>" % (self.__class__.__name__, self.name, self.count, mapi.HexFromBin(self.id[0]), mapi.HexFromBin(self.id[1])) def __eq__(self, other): if other is None: return False ceid = self.msgstore.session.CompareEntryIDs return ceid(self.id[0], other.id[0]) and \ ceid(self.id[1], other.id[1]) def __ne__(self, other): return not self.__eq__(other) def GetID(self): return mapi.HexFromBin(self.id[0]), mapi.HexFromBin(self.id[1]) def GetFQName(self): parts = [] parent = self while parent is not None: parts.insert(0, parent.name) try: # Ignore errors fetching parents - the caller just wants the # name - it may not be correctly 'fully qualified', but at # least we get something. parent = parent.GetParent() except MsgStoreException: break # We now end up with [0] being an empty string??, [1] being the # information store root folder name, etc. Outlook etc all just # use the information store name here. if parts and not parts[0]: del parts[0] # Don't catch exceptions on the item itself - that is fatal, # and should be caught by the caller. # Replace the "root" folder name with the information store name # as Outlook, our Folder selector etc do. mapi_store = self.msgstore._GetMessageStore(self.id[0]) hr, data = mapi_store.GetProps((PR_DISPLAY_NAME_A,), 0) name = data[0][1] if parts: # and replace with new name parts[0] = name else: # This can happen for the very root folder (ie, parent of the # top-level folder shown by Outlook. This folder should *never* # be used directly. parts = [name] print "WARNING: It appears you are using the top-level root of " \ "the information store as a folder. You probably don't "\ "want to do that" return "/".join(parts) def _FolderFromMAPIFolder(self, mapifolder): # Finally get the display name. hr, data = mapifolder.GetProps((PR_ENTRYID, PR_DISPLAY_NAME_A,), 0) eid = self.id[0], data[0][1] name = data[1][1] count = mapifolder.GetContentsTable(0).GetRowCount(0) return MAPIMsgStoreFolder(self.msgstore, eid, name, count) def GetParent(self): # return a folder object with the parent, or None if there is no # parent (ie, a top-level folder). Raises an exception if there is # an error fetching the parent (which implies something wrong with the # item itself, rather than this being top-level) try: folder = self.msgstore._OpenEntry(self.id) prop_ids = PR_PARENT_ENTRYID, hr, data = folder.GetProps(prop_ids,0) # Put parent ids together parent_eid = data[0][1] parent_id = self.id[0], parent_eid if hr != 0 or \ self.msgstore.session.CompareEntryIDs(parent_eid, self.id[1]): # No parent EID, or EID same as ours. return None parent = self.msgstore._OpenEntry(parent_id) # Finally get the item itself return self._FolderFromMAPIFolder(parent) except pythoncom.com_error, details: raise MsgStoreExceptionFromCOMException(details) def OpenEntry(self, iid = None, flags = None): return self.msgstore._OpenEntry(self.id, iid, flags) def GetOutlookItem(self): try: hex_item_id = mapi.HexFromBin(self.id[1]) hex_store_id = mapi.HexFromBin(self.id[0]) return self.msgstore.outlook.Session.GetFolderFromID(hex_item_id, hex_store_id) except pythoncom.com_error, details: raise MsgStoreExceptionFromCOMException(details) def GetMessageGenerator(self, only_filter_candidates = True): folder = self.OpenEntry() table = folder.GetContentsTable(0) table.SetColumns(MAPIMsgStoreMsg.message_init_props, 0) if only_filter_candidates: # Limit ourselves to IPM.* objects - ie, messages. restriction = (mapi.RES_PROPERTY, # a property restriction (mapi.RELOP_GE, # >= PR_MESSAGE_CLASS_A, # of the this prop (PR_MESSAGE_CLASS_A, "IPM."))) # with this value table.Restrict(restriction, 0) while 1: # Getting 70 at a time was the random number that gave best # perf for me ;) rows = table.QueryRows(70, 0) if len(rows) == 0: break for row in rows: # Our restriction helped, but may not have filtered # every message we don't want to touch. # Note no exception will be raised below if the message is # moved under us, as we don't need to access any properties. msg = MAPIMsgStoreMsg(self.msgstore, row) if not only_filter_candidates or msg.IsFilterCandidate(): yield msg def GetNewUnscoredMessageGenerator(self, scoreFieldName): folder = self.msgstore._OpenEntry(self.id) table = folder.GetContentsTable(0) # Resolve the field name resolve_props = ( (mapi.PS_PUBLIC_STRINGS, scoreFieldName), ) resolve_ids = folder.GetIDsFromNames(resolve_props, 0) field_id = PROP_TAG( PT_DOUBLE, PROP_ID(resolve_ids[0])) # Setup the properties we want to read. table.SetColumns(MAPIMsgStoreMsg.message_init_props, 0) # Set up the restriction # Need to check message-flags # (PR_CONTENT_UNREAD is optional, and somewhat unreliable # PR_MESSAGE_FLAGS & MSGFLAG_READ is the official way) prop_restriction = (mapi.RES_BITMASK, # a bitmask restriction (mapi.BMR_EQZ, # when bit is clear PR_MESSAGE_FLAGS, MSGFLAG_READ)) exist_restriction = mapi.RES_EXIST, (field_id,) not_exist_restriction = mapi.RES_NOT, (exist_restriction,) # A restriction for the message class class_restriction = (mapi.RES_PROPERTY, # a property restriction (mapi.RELOP_GE, # >= PR_MESSAGE_CLASS_A, # of the this prop (PR_MESSAGE_CLASS_A, "IPM."))) # with this value # Put the final restriction together restriction = (mapi.RES_AND, (prop_restriction, not_exist_restriction, class_restriction)) table.Restrict(restriction, 0) while 1: rows = table.QueryRows(70, 0) if len(rows) == 0: break for row in rows: # Note no exception will be raised below if the message is # moved under us, as we don't need to access any properties. msg = MAPIMsgStoreMsg(self.msgstore, row) if msg.IsFilterCandidate(): yield msg def IsReceiveFolder(self, msg_class = "IPM.Note"): # Is this folder the nominated "receive folder" for its store? try: mapi_store = self.msgstore._GetMessageStore(self.id[0]) eid, ret_class = mapi_store.GetReceiveFolder(msg_class, 0) return mapi_store.CompareEntryIDs(eid, self.id[1]) except pythoncom.com_error: # Error getting the receive folder from the store (or maybe our # store - but that would be insane!). Either way, we can't be it! return False def CreateFolder(self, name, comments = None, type = None, open_if_exists = False, flags = None): if type is None: type = mapi.FOLDER_GENERIC if flags is None: flags = 0 if open_if_exists: flags |= mapi.OPEN_IF_EXISTS folder = self.OpenEntry() ret = folder.CreateFolder(type, name, comments, None, flags) return self._FolderFromMAPIFolder(ret) def GetItemCount(self): try: folder = self.OpenEntry() return folder.GetContentsTable(0).GetRowCount(0) except pythoncom.com_error, details: raise MsgStoreExceptionFromCOMException(details) # EmptyFolder() *permanently* deletes ALL messages and subfolders from # this folder without deleting the folder itself. # # WORD OF WARNING: This is a *very dangerous* function that has the # potential to destroy a user's mail. Don't even *think* about calling # this function on anything but the Certain Spam folder! def EmptyFolder(self, parentWindow): try: folder = self.OpenEntry() folder.EmptyFolder(parentWindow, None, FOLDER_DIALOG) except pythoncom.com_error, details: raise MsgStoreExceptionFromCOMException(details) def DoesFolderHaveOutlookField(self, field_name): # Returns True if the specified folder has an *Outlook* field with # the given name, False if the folder does not have it, or None # if we can't tell, or there was an error, etc. # We have discovered that Outlook stores 'Fields' for a folder as a # PR_USERFIELDS field in the hidden, 'associated' message with # message class IPC.MS.REN.USERFIELDS. This is a binary property # which is undocumented, but probably could be reverse-engineered # with a little effort (see 'dump_props --dump-folder-user-props' for # an example of the raw data. For now, the simplest thing appears # to be to check for a \0 character, followed by the property name # as an ascii string. try: folder = self.msgstore._OpenEntry(self.id) table = folder.GetContentsTable(mapi.MAPI_ASSOCIATED) restriction = (mapi.RES_PROPERTY, (mapi.RELOP_EQ, PR_MESSAGE_CLASS_A, (PR_MESSAGE_CLASS_A, 'IPC.MS.REN.USERFIELDS'))) cols = (PR_USERFIELDS,) table.SetColumns(cols, 0) rows = mapi.HrQueryAllRows(table, cols, restriction, None, 0) if len(rows)>1: print "Eeek - only expecting one row from IPC.MS.REN.USERFIELDS" print "got", repr(rows) return None if len(rows)==0: # New folders with no userdefined fields do not have such a row, # but this is a clear indication it does not exist. return False row = rows[0] val = GetPotentiallyLargeStringProp(folder, cols[0], row[0]) except pythoncom.com_error, details: raise MsgStoreExceptionFromCOMException(details) if type(val) != type(''): print "Value type incorrect - expected string, got", repr(val) return None return val.find("\0" + field_name) >= 0 def DeleteMessages(self, message_things): # A *permanent* delete - MAPI has no concept of 'Deleted Items', # only Outlook does. If you want a "soft" delete, you must locate # deleted item (via a special ID) and move it to there yourself # message_things may be ID tuples, or MAPIMsgStoreMsg instances. real_ids = [] for thing in message_things: if isinstance(thing, MAPIMsgStoreMsg): real_ids.append( thing.id[1] ) thing.mapi_object = thing.id = thing.folder_id = None else: real_ids.append(self.msgstore.NormalizeID(thing)[1]) try: folder = self.msgstore._OpenEntry(self.id) # Nuke my MAPI reference, and set my ID to None folder.DeleteMessages(real_ids, 0, None, 0) except pythoncom.com_error, details: raise MsgStoreExceptionFromCOMException(details) def CreateTemporaryMessage(self, msg_flags = None): # Create a message designed to be used temporarily. It is your # responsibility to delete when you are done with it. # If msg_flags is not None, it should be an integer for the # PR_MESSAGE_FLAGS property. Note that Outlook appears to refuse # to set user properties on a message marked as 'unsent', which # is the default. Setting to, eg, 1 marks it as a "not unsent, read" # message, which works fine with user properties. try: folder = self.msgstore._OpenEntry(self.id) imsg = folder.CreateMessage(None, 0) if msg_flags is not None: props = (PR_MESSAGE_FLAGS,msg_flags), imsg.SetProps(props) imsg.SaveChanges(0) hr, data = imsg.GetProps((PR_ENTRYID, PR_STORE_ENTRYID), 0) eid = data[0][1] storeid = data[1][1] msg_id = mapi.HexFromBin(storeid), mapi.HexFromBin(eid) except pythoncom.com_error, details: raise MsgStoreExceptionFromCOMException(details) return self.msgstore.GetMessage(msg_id) class MAPIMsgStoreMsg: # All the properties we must initialize a message with. # These include all the IDs we need, parent IDs, any properties needed # to determine if this is a "filterable" message, etc message_init_props = (PR_ENTRYID, PR_STORE_ENTRYID, PR_SEARCH_KEY, PR_PARENT_ENTRYID, # folder ID PR_MESSAGE_CLASS_A, # 'IPM.Note' etc PR_RECEIVED_BY_ENTRYID, # who received it PR_SUBJECT_A, PR_TRANSPORT_MESSAGE_HEADERS_A, ) def __init__(self, msgstore, prop_row): self.msgstore = msgstore self.mapi_object = None # prop_row is a single mapi property row, with fields as above. # NOTE: We can't trust these properties for "large" values # (ie, strings, PT_BINARY, objects etc.), as they sometimes come # from the IMAPITable (which has a 255 limit on property values) # and sometimes from the object itself (which has no restriction). # This limitation is documented by MAPI. # Thus, we don't trust "PR_TRANSPORT_MESSAGE_HEADERS_A" more than # to ask "does the property exist?" tag, eid = prop_row[0] # ID tag, store_eid = prop_row[1] tag, searchkey = prop_row[2] tag, parent_eid = prop_row[3] tag, msgclass = prop_row[4] recby_tag, recby = prop_row[5] tag, subject = prop_row[6] headers_tag, headers = prop_row[7] self.id = store_eid, eid self.folder_id = store_eid, parent_eid self.msgclass = msgclass self.subject = subject has_headers = PROP_TYPE(headers_tag)==PT_STRING8 # Search key is the only reliable thing after a move/copy operation # only problem is that it can potentially be changed - however, the # Outlook client provides no such (easy/obvious) way # (ie, someone would need to really want to change it ) # Thus, searchkey is our long-lived message key. self.searchkey = searchkey # To check if a message has ever been received, we check the # PR_RECEIVED_BY_ENTRYID flag. Tim wrote in an old comment that # An article on the web said the distinction can't be made with 100% # certainty, but that a good heuristic is to believe that a # msg has been received iff at least one of these properties # has a sensible value: RECEIVED_BY_EMAIL_ADDRESS, RECEIVED_BY_NAME, # RECEIVED_BY_ENTRYID PR_TRANSPORT_MESSAGE_HEADERS # But MarkH can't find it, and believes and tests that # PR_RECEIVED_BY_ENTRYID is all we need (but has since discovered a # couple of messages without any PR_RECEIVED_BY properties - but *with* # PR_TRANSPORT_MESSAGE_HEADERS - *sigh*) self.was_received = PROP_TYPE(recby_tag) == PT_BINARY or has_headers self.dirty = False # For use with the spambayes.message messageinfo database. self.stored_attributes = ['c', 't', 'original_folder', 'date_modified'] self.t = None self.c = None self.date_modified = None self.original_folder = None def getDBKey(self): # Long lived search key. return self.searchkey def __repr__(self): if self.id is None: id_str = "(deleted/moved)" else: id_str = mapi.HexFromBin(self.id[0]), mapi.HexFromBin(self.id[1]) return "<%s, '%s' id=%s>" % (self.__class__.__name__, self.GetSubject(), id_str) # as per search-key comments above, we also "enforce" this at the Python # level. 2 different messages, but one copied from the other, will # return "==". # Not being consistent could cause subtle bugs, especially in interactions # with various test tools. # Compare the GetID() results if you need to know different messages. def __hash__(self): return hash(self.searchkey) def __eq__(self, other): ceid = self.msgstore.session.CompareEntryIDs return ceid(self.searchkey, other.searchkey) def __ne__(self, other): return not self.__eq__(other) def GetID(self): return mapi.HexFromBin(self.id[0]), mapi.HexFromBin(self.id[1]) def GetSubject(self): return self.subject def GetOutlookItem(self): hex_item_id = mapi.HexFromBin(self.id[1]) hex_store_id = mapi.HexFromBin(self.id[0]) return self.msgstore.outlook.Session.GetItemFromID(hex_item_id, hex_store_id) def IsFilterCandidate(self): # We don't attempt to filter: # * Non-mail items # * Messages that weren't actually received - this generally means user # composed messages yet to be sent, or copies of "sent items". # It does *not* exclude messages that were user composed, but still # actually received by the user (ie, when you mail yourself) # GroupWise generates IPM.Anti-Virus.Report.45 (but I'm not sure how # it manages given it is an external server, and as far as I can tell, # this does not appear in the headers. if test_suite_running: # While the test suite is running, we *only* filter test msgs. return self.subject == "SpamBayes addin auto-generated test message" class_check = self.msgclass.lower() for check in "ipm.note", "ipm.anti-virus": if class_check.startswith(check): break else: # Not matching class - no good return False # Must match msg class to get here. return self.was_received def _GetPotentiallyLargeStringProp(self, prop_id, row): return GetPotentiallyLargeStringProp(self.mapi_object, prop_id, row) def _GetMessageText(self): parts = self._GetMessageTextParts() # parts is (headers, body, html) - which needs more formalizing - # GetMessageText should become deprecated - it makes no sense in the # face of multi-part messages. return "\n".join(parts) def _GetMessageTextParts(self): # This is almost reliable :). The only messages this now fails for # are for "forwarded" messages, where the forwards are actually # in an attachment. Later. # Note we *dont* look in plain text attachments, which we arguably # should. # This should be refactored into a function that returns the headers, # plus a list of email package sub-objects suitable for sending to # the classifier. from spambayes import mboxutils self._EnsureObject() prop_ids = (PR_BODY_A, MYPR_BODY_HTML_A, PR_TRANSPORT_MESSAGE_HEADERS_A) hr, data = self.mapi_object.GetProps(prop_ids,0) body = self._GetPotentiallyLargeStringProp(prop_ids[0], data[0]) html = self._GetPotentiallyLargeStringProp(prop_ids[1], data[1]) headers = self._GetPotentiallyLargeStringProp(prop_ids[2], data[2]) # xxx - not sure what to do if we have both. if not html: html = GetHTMLFromRTFProperty(self.mapi_object) # Some Outlooks deliver a strange notion of headers, including # interior MIME armor. To prevent later errors, try to get rid # of stuff now that can't possibly be parsed as "real" (SMTP) # headers. headers = mboxutils.extract_headers(headers) # Mail delivered internally via Exchange Server etc may not have # headers - fake some up. if not headers: headers = self._GetFakeHeaders() # Mail delivered via the Exchange Internet Mail MTA may have # gibberish at the start of the headers - fix this. elif headers.startswith("Microsoft Mail"): headers = "X-MS-Mail-Gibberish: " + headers # This mail typically doesn't have a Received header, which # is a real PITA for running the incremental testing setup. # To make life easier, we add in the fake one that the message # would have got if it had had no headers at all. if headers.find("Received:") == -1: prop_ids = PR_MESSAGE_DELIVERY_TIME hr, data = self.mapi_object.GetProps(prop_ids, 0) value = self._format_received(data[0][1]) headers = "Received: %s\n%s" % (value, headers) if not html and not body: # Only ever seen this for "multipart/signed" messages, so # without any better clues, just handle this. # Find all attachments with # PR_ATTACH_MIME_TAG_A=multipart/signed # XXX - see also self._GetAttachmentsToInclude(), which # scans the attachment table - we should consolidate! table = self.mapi_object.GetAttachmentTable(0) restriction = (mapi.RES_PROPERTY, # a property restriction (mapi.RELOP_EQ, # check for equality PR_ATTACH_MIME_TAG_A, # of the given prop (PR_ATTACH_MIME_TAG_A, "multipart/signed"))) try: rows = mapi.HrQueryAllRows(table, (PR_ATTACH_NUM,), # columns to get restriction, # only these rows None, # any sort order is fine 0) # any # of results is fine except pythoncom.com_error: # For some reason there are no rows we can get rows = [] if len(rows) == 0: pass # Nothing we can fetch :( else: if len(rows) > 1: print "WARNING: Found %d rows with multipart/signed" \ "- using first only" % len(rows) row = rows[0] (attach_num_tag, attach_num), = row assert attach_num_tag != PT_ERROR, \ "Error fetching attach_num prop" # Open the attachment attach = self.mapi_object.OpenAttach(attach_num, None, mapi.MAPI_DEFERRED_ERRORS) prop_ids = (PR_ATTACH_DATA_BIN,) hr, data = attach.GetProps(prop_ids, 0) attach_body = GetPotentiallyLargeStringProp(attach, prop_ids[0], data[0]) # What we seem to have here now is a *complete* multi-part # mime message - that Outlook must have re-constituted on # the fly immediately after pulling it apart! - not unlike # exactly what we are doing ourselves right here - putting # it into a message object, so we can extract the text, so # we can stick it back into another one. Ahhhhh. msg = email.message_from_string(attach_body) assert msg.is_multipart(), "Should be multi-part: %r" % attach_body # reduce down all sub messages, collecting all text/ subtypes. # (we could make a distinction between text and html, but # it is all joined together by this method anyway.) def collect_text_parts(msg): collected = '' if msg.is_multipart(): for sub in msg.get_payload(): collected += collect_text_parts(sub) else: if msg.get_content_maintype()=='text': collected += msg.get_payload() else: #print "skipping content type", msg.get_content_type() pass return collected body = collect_text_parts(msg) return headers, body, html def _GetFakeHeaders(self): # This is designed to fake up some SMTP headers for messages # on an exchange server that do not have such headers of their own. prop_ids = PR_SUBJECT_A, PR_SENDER_NAME_A, PR_DISPLAY_TO_A, \ PR_DISPLAY_CC_A, PR_MESSAGE_DELIVERY_TIME, \ MYPR_MESSAGE_ID_A, PR_IMPORTANCE, PR_CLIENT_SUBMIT_TIME, hr, data = self.mapi_object.GetProps(prop_ids, 0) headers = ["X-Exchange-Message: true"] for header, index, potentially_large, format_func in (\ ("Subject", 0, True, None), ("From", 1, True, self._format_address), ("To", 2, True, self._format_address), ("CC", 3, True, self._format_address), ("Received", 4, False, self._format_received), ("Message-ID", 5, True, None), ("Importance", 6, False, self._format_importance), ("Date", 7, False, self._format_time), ("X-Mailer", 7, False, self._format_version), ): if potentially_large: value = self._GetPotentiallyLargeStringProp(prop_ids[index], data[index]) else: value = data[index][1] if value: if format_func: value = format_func(value) headers.append("%s: %s" % (header, value)) return "\n".join(headers) + "\n" def _format_received(self, raw): # Fake up a 'received' header. It's important that the date # is right, so that sort+group.py will work. The rest is just more # clues for the tokenizer to find. return "(via local Exchange server); %s" % (self._format_time(raw),) def _format_time(self, raw): return formatdate(int(raw)-timezone, True) def _format_importance(self, raw): # olImportanceHigh = 2, olImportanceLow = 0, olImportanceNormal = 1 return {0 : "low", 1 : "normal", 2 : "high"}[raw] def _format_version(self, unused): return "Microsoft Exchange Client" _address_re = re.compile(r"[()<>,:@!/=; ]") def _format_address(self, raw): # Fudge up something that's in the appropriate form. We don't # have enough information available to get an actual working # email address. addresses = raw.split(";") formattedAddresses = [] for address in addresses: address = address.strip() if address.find("@") >= 0: formattedAddress = address else: formattedAddress = "\"%s\" <%s>" % \ (address, self._address_re.sub('.', address)) formattedAddresses.append(formattedAddress) return "; ".join(formattedAddresses) def _EnsureObject(self): if self.mapi_object is None: try: help_test_suite("MAPIMsgStoreMsg._EnsureObject") self.mapi_object = self.msgstore._OpenEntry(self.id) except pythoncom.com_error, details: raise MsgStoreExceptionFromCOMException(details) def _GetAttachmentsToInclude(self): # Get the list of attachments to include in the email package # Message object. Currently only images (BUT - consider consolidating # with the attachment handling above for signed messages!) from spambayes.Options import options from spambayes.ImageStripper import image_large_size_attribute # For now, we know these are the only 2 options that need attachments. if not options['Tokenizer', 'crack_images'] and \ not options['Tokenizer', 'image_size']: return [] try: table = self.mapi_object.GetAttachmentTable(0) tags = PR_ATTACH_NUM,PR_ATTACH_MIME_TAG_A,PR_ATTACH_SIZE,PR_ATTACH_DATA_BIN attach_rows = mapi.HrQueryAllRows(table, tags, None, None, 0) except pythoncom.com_error, why: attach_rows = [] attachments = [] # Create a new attachment for each image. for row in attach_rows: attach_num = row[0][1] # mime-tag may not exist - eg, seen on bounce messages mime_tag = None if PROP_TYPE(row[1][0]) != PT_ERROR: mime_tag = row[1][1] # oh - what is the library for this!? if mime_tag: typ, subtyp = mime_tag.split('/', 1) if typ == 'image': size = row[2][1] # If it is too big, just write the size. ImageStripper.py # checks this attribute. if size > options["Tokenizer", "max_image_size"]: sub = MIMEImage(None, subtyp) setattr(sub, image_large_size_attribute, size) else: attach = self.mapi_object.OpenAttach(attach_num, None, mapi.MAPI_DEFERRED_ERRORS) data = GetPotentiallyLargeStringProp(attach, PR_ATTACH_DATA_BIN, row[3]) sub = MIMEImage(data, subtyp) attachments.append(sub) return attachments def GetEmailPackageObject(self, strip_mime_headers=True): # Return an email.Message object. # # strip_mime_headers is a hack, and should be left True unless you're # trying to display all the headers for diagnostic purposes. If we # figure out something better to do, it should go away entirely. # # Problem #1: suppose a msg is multipart/alternative, with # text/plain and text/html sections. The latter MIME decorations # are plain missing in what _GetMessageText() returns. If we leave # the multipart/alternative in the headers anyway, the email # package's "lax parsing" won't complain about not finding any # sections, but since the type *is* multipart/alternative then # anyway, the tokenizer finds no text/* parts at all to tokenize. # As a result, only the headers get tokenized. By stripping # Content-Type from the headers (if present), the email pkg # considers the body to be text/plain (the default), and so it # does get tokenized. # # Problem #2: Outlook decodes quoted-printable and base64 on its # own, but leaves any Content-Transfer-Encoding line in the headers. # This can cause the email pkg to try to decode the text again, # with unpleasant (but rarely fatal) results. If we strip that # header too, no problem -- although the fact that a msg was # encoded in base64 is usually a good spam clue, and we miss that. # # Short course: we either have to synthesize non-insane MIME # structure, or eliminate all evidence of original MIME structure. # We used to do the latter - but now that we must give valid # multipart messages which include attached images, we are forced # to try and do the former (but actually the 2 options are not # mutually exclusive - first we eliminate all evidence of original # MIME structure, before allowing the email package to synthesize # non-insane MIME structure. # We still jump through hoops though - if we have no interesting # attachments we attempt to return as close as possible as what # we always returned in the past - a "single-part" message with the # text and HTML as a simple text body. header_text, body, html = self._GetMessageTextParts() try: # catch all exceptions! # Try and decide early if we want multipart or not. # We originally just looked at the content-type - but Outlook # is unreliable WRT that header! Also, consider a message multipart message # with only text and html sections and no additional attachments. # Outlook will generally have copied the HTML and Text sections # into the relevant properties and they will *not* appear as # attachments. We should return the 'single' message here to keep # as close to possible to what we used to return. We can change # this policy in the future - but we would probably need to insist # on a full re-train as the training tokens will have changed for # many messages. attachments = self._GetAttachmentsToInclude() new_content_type = None if attachments: _class = MIMEMultipart payload = [] if body: payload.append(MIMEText(body)) if html: payload.append(MIMEText(html, 'html')) payload += attachments new_content_type = "multipart/mixed" else: # Single message part with both text and HTML. _class = Message payload = body + '\n' + html try: root_msg = HeaderParser(_class=_class).parsestr(header_text) except email.Errors.HeaderParseError: raise # sob # ack - it is about here we need to do what the old code did # below: But - the fact the code below is dealing only # with content-type (and the fact we handle that above) makes # it less obvious.... ## But even this doesn't get *everything*. We can still see: ## "multipart message with no defined boundary" or the ## HeaderParseError above. Time to get brutal - hack out ## the Content-Type header, so we see it as plain text. #if msg is None: # butcher_pos = text.lower().find("\ncontent-type: ") # if butcher_pos < 0: # # This error just just gunna get caught below anyway # raise RuntimeError( # "email package croaked with a MIME related error, but " # "there appears to be no 'Content-Type' header") # # Put it back together, skipping the original "\n" but # # leaving the header leaving "\nSpamBayes-Content-Type: " # butchered = text[:butcher_pos] + "\nSpamBayes-" + \ # text[butcher_pos+1:] + "\n\n" # msg = email.message_from_string(butchered) # patch up mime stuff - these headers will confuse the email # package as it walks the attachments. if strip_mime_headers: for h, new_val in (('content-type', new_content_type), ('content-transfer-encoding', None)): try: root_msg['X-SpamBayes-Original-' + h] = root_msg[h] del root_msg[h] except KeyError: pass if new_val is not None: root_msg[h] = new_val root_msg.set_payload(payload) # We used to call email.message_from_string(text) and catch: # email.Errors.BoundaryError: should no longer happen - we no longer # ask the email package to parse anything beyond headers. # email.Errors.HeaderParseError: caught above except: text = '\r\n'.join([header_text, body, html]) print "FAILED to create email.message from: ", `text` raise return root_msg # XXX - this is the OLD version of GetEmailPackageObject() - it # temporarily remains as a testing aid, to ensure that the different # mime structure we now generate has no negative affects. # Use 'sandbox/export.py -o' to export to the testdata directory # in the old format, then run the cross-validation tests. def OldGetEmailPackageObject(self, strip_mime_headers=True): # Return an email.Message object. # # strip_mime_headers is a hack, and should be left True unless you're # trying to display all the headers for diagnostic purposes. If we # figure out something better to do, it should go away entirely. # # Problem #1: suppose a msg is multipart/alternative, with # text/plain and text/html sections. The latter MIME decorations # are plain missing in what _GetMessageText() returns. If we leave # the multipart/alternative in the headers anyway, the email # package's "lax parsing" won't complain about not finding any # sections, but since the type *is* multipart/alternative then # anyway, the tokenizer finds no text/* parts at all to tokenize. # As a result, only the headers get tokenized. By stripping # Content-Type from the headers (if present), the email pkg # considers the body to be text/plain (the default), and so it # does get tokenized. # # Problem #2: Outlook decodes quoted-printable and base64 on its # own, but leaves any Content-Transfer-Encoding line in the headers. # This can cause the email pkg to try to decode the text again, # with unpleasant (but rarely fatal) results. If we strip that # header too, no problem -- although the fact that a msg was # encoded in base64 is usually a good spam clue, and we miss that. # # Short course: we either have to synthesize non-insane MIME # structure, or eliminate all evidence of original MIME structure. # Since we don't have a way to the former, by default this function # does the latter. import email text = self._GetMessageText() try: try: msg = email.message_from_string(text) except email.Errors.BoundaryError: # In case this is the # "No terminating boundary and no trailing empty line" # flavor of BoundaryError, we can supply a trailing empty # line to shut it up. It's certainly ill-formed MIME, and # probably spam. We don't care about the exact MIME # structure, just the words it contains, so no harm and # much good in trying to suppress this error. try: msg = email.message_from_string(text + "\n\n") except email.Errors.BoundaryError: msg = None except email.Errors.HeaderParseError: # This exception can come from parsing the header *or* the # body of a mime message. msg = None # But even this doesn't get *everything*. We can still see: # "multipart message with no defined boundary" or the # HeaderParseError above. Time to get brutal - hack out # the Content-Type header, so we see it as plain text. if msg is None: butcher_pos = text.lower().find("\ncontent-type: ") if butcher_pos < 0: # This error just just gunna get caught below anyway raise RuntimeError( "email package croaked with a MIME related error, but " "there appears to be no 'Content-Type' header") # Put it back together, skipping the original "\n" but # leaving the header leaving "\nSpamBayes-Content-Type: " butchered = text[:butcher_pos] + "\nSpamBayes-" + \ text[butcher_pos+1:] + "\n\n" msg = email.message_from_string(butchered) except: print "FAILED to create email.message from: ", `text` raise if strip_mime_headers: if msg.has_key('content-type'): del msg['content-type'] if msg.has_key('content-transfer-encoding'): del msg['content-transfer-encoding'] return msg # end of OLD GetEmailPackageObject def SetField(self, prop, val): # Future optimization note - from GetIDsFromNames doco # Name-to-identifier mapping is represented by an object's # PR_MAPPING_SIGNATURE property. PR_MAPPING_SIGNATURE contains # a MAPIUID structure that indicates the service provider # responsible for the object. If the PR_MAPPING_SIGNATURE # property is the same for two objects, assume that these # objects use the same name-to-identifier mapping. # [MarkH: MAPIUID objects are supported and hashable] # XXX If the SpamProb (Hammie, whatever) property is passed in as an # XXX int, Outlook displays the field as all blanks, and sorting on # XXX it doesn't do anything, etc. I don't know why. Since I'm # XXX running Python 2.2.2, the _MapiTypeMap above confuses ints # XXX with bools, but the problem persists even if I comment out the # XXX PT_BOOLEAN entry from that dict. Dumping in prints below show # XXX that type_tag is 3 then, and that matches the defn of PT_I4 in # XXX my system header files. # XXX Later: This works after all, but the field shows up as all # XXX blanks unless I *first* modify the view (like Messages) in # XXX Outlook to define a custom Integer field of the same name. self._EnsureObject() try: if type(prop) != type(0): props = ( (mapi.PS_PUBLIC_STRINGS, prop), ) propIds = self.mapi_object.GetIDsFromNames(props, mapi.MAPI_CREATE) type_tag = _MapiTypeMap.get(type(val)) if type_tag is None: raise ValueError, "Don't know what to do with '%r' ('%s')" % ( val, type(val)) prop = PROP_TAG(type_tag, PROP_ID(propIds[0])) help_test_suite("MAPIMsgStoreMsg.SetField") if val is None: # Delete the property self.mapi_object.DeleteProps((prop,)) else: self.mapi_object.SetProps(((prop,val),)) self.dirty = True except pythoncom.com_error, details: raise MsgStoreExceptionFromCOMException(details) def GetField(self, prop): # xxx - still raise_errors? self._EnsureObject() if type(prop) != type(0): props = ( (mapi.PS_PUBLIC_STRINGS, prop), ) prop = self.mapi_object.GetIDsFromNames(props, 0)[0] if PROP_TYPE(prop) == PT_ERROR: # No such property return None prop = PROP_TAG( PT_UNSPECIFIED, PROP_ID(prop)) try: hr, props = self.mapi_object.GetProps((prop,), 0) ((tag, val), ) = props if PROP_TYPE(tag) == PT_ERROR: if val == mapi.MAPI_E_NOT_ENOUGH_MEMORY: # Too big for simple properties - get via a stream return GetPropFromStream(self.mapi_object, prop) return None return val except pythoncom.com_error, details: raise MsgStoreExceptionFromCOMException(details) def GetReadState(self): val = self.GetField(PR_MESSAGE_FLAGS) return (val&MSGFLAG_READ) != 0 def SetReadState(self, is_read): try: self._EnsureObject() # always try and clear any pending delivery reports of read/unread help_test_suite("MAPIMsgStoreMsg.SetReadState") if is_read: self.mapi_object.SetReadFlag(USE_DEFERRED_ERRORS|SUPPRESS_RECEIPT) else: self.mapi_object.SetReadFlag(USE_DEFERRED_ERRORS|CLEAR_READ_FLAG) if __debug__: if self.GetReadState() != is_read: print "MAPI SetReadState appears to have failed to change the message state" print "Requested set to %s but the MAPI field after was %r" % \ (is_read, self.GetField(PR_MESSAGE_FLAGS)) except pythoncom.com_error, details: raise MsgStoreExceptionFromCOMException(details) def Save(self): assert self.dirty, "asking me to save a clean message!" # It seems that *not* specifying mapi.MAPI_DEFERRED_ERRORS solves a lot # problems! So we don't! try: help_test_suite("MAPIMsgStoreMsg.Save") self.mapi_object.SaveChanges(mapi.KEEP_OPEN_READWRITE) self.dirty = False except pythoncom.com_error, details: raise MsgStoreExceptionFromCOMException(details) def _DoCopyMove(self, folder, isMove): assert not self.dirty, \ "asking me to move a dirty message - later saves will fail!" try: dest_folder = self.msgstore._OpenEntry(folder.id) source_folder = self.msgstore._OpenEntry(self.folder_id) flags = 0 if isMove: flags |= MESSAGE_MOVE eid = self.id[1] help_test_suite("MAPIMsgStoreMsg._DoCopyMove") source_folder.CopyMessages((eid,), None, dest_folder, 0, None, flags) # At this stage, I think we have lost meaningful ID etc values # Set everything to None to make it clearer what is wrong should # this become an issue. We would need to re-fetch the eid of # the item, and set the store_id to the dest folder. self.id = None self.folder_id = None except pythoncom.com_error, details: raise MsgStoreExceptionFromCOMException(details) def MoveTo(self, folder): self._DoCopyMove(folder, True) def CopyTo(self, folder): self._DoCopyMove(folder, False) # Functions to perform operations, but report the error (ONCE!) to the # user. Any errors are re-raised so the caller can degrade gracefully if # necessary. # XXX - not too happy with these - they should go, and the caller should # handle (especially now that we work exclusively with exceptions from # this module. def MoveToReportingError(self, manager, folder): try: self.MoveTo(folder) except MsgStoreException, details: ReportMAPIError(manager, _("Moving a message"), details.mapi_exception) def CopyToReportingError(self, manager, folder): try: self.MoveTo(folder) except MsgStoreException, details: ReportMAPIError(manager, _("Copying a message"), details.mapi_exception) def GetFolder(self): # return a folder object with the parent, or None folder_id = (mapi.HexFromBin(self.folder_id[0]), mapi.HexFromBin(self.folder_id[1])) return self.msgstore.GetFolder(folder_id) def RememberMessageCurrentFolder(self): self._EnsureObject() try: folder = self.GetFolder() # Also save this information in our messageinfo database, which # means that restoring should work even with IMAP. self.original_folder = folder.id[0], folder.id[1] props = ( (mapi.PS_PUBLIC_STRINGS, "SpamBayesOriginalFolderStoreID"), (mapi.PS_PUBLIC_STRINGS, "SpamBayesOriginalFolderID") ) resolve_ids = self.mapi_object.GetIDsFromNames(props, mapi.MAPI_CREATE) prop_ids = PROP_TAG( PT_BINARY, PROP_ID(resolve_ids[0])), \ PROP_TAG( PT_BINARY, PROP_ID(resolve_ids[1])) prop_tuples = (prop_ids[0],folder.id[0]), (prop_ids[1],folder.id[1]) self.mapi_object.SetProps(prop_tuples) self.dirty = True except pythoncom.com_error, details: raise MsgStoreExceptionFromCOMException(details) def GetRememberedFolder(self): props = ( (mapi.PS_PUBLIC_STRINGS, "SpamBayesOriginalFolderStoreID"), (mapi.PS_PUBLIC_STRINGS, "SpamBayesOriginalFolderID") ) try: self._EnsureObject() resolve_ids = self.mapi_object.GetIDsFromNames(props, mapi.MAPI_CREATE) prop_ids = PROP_TAG( PT_BINARY, PROP_ID(resolve_ids[0])), \ PROP_TAG( PT_BINARY, PROP_ID(resolve_ids[1])) hr, data = self.mapi_object.GetProps(prop_ids,0) if hr != 0: return None (store_tag, store_id), (eid_tag, eid) = data folder_id = mapi.HexFromBin(store_id), mapi.HexFromBin(eid) help_test_suite("MAPIMsgStoreMsg.GetRememberedFolder") return self.msgstore.GetFolder(folder_id) except: # Try to get it from the message info database, if possible if self.original_folder: return self.msgstore.GetFolder(self.original_folder) print "Error locating origin of message", self return None def test(): outlook = Dispatch("Outlook.Application") inbox = outlook.Session.GetDefaultFolder(constants.olFolderInbox) folder_id = inbox.Parent.StoreID, inbox.EntryID store = MAPIMsgStore() for folder in store.GetFolderGenerator([folder_id,], True): print folder for msg in folder.GetMessageGenerator(): print msg store.Close() if __name__=='__main__': test() spambayes-1.1a6/Outlook2000/README.txt0000664000076500000240000001004510646440136017321 0ustar skipstaff00000000000000This directory contains tools for using the classifier with Microsoft Outlook 2000, 2002, and 2003, courtesy of Sean True and Mark Hammond. Note that you need Python's win32com extensions (http://starship.python.net/crew/mhammond) and you *must* have win32all-149 or later. Note that running "setup.py install" will *not* install the contents of this directory into the Python site-packages directory. You will need to either copy this directory there yourself, or run it from some other appropriate location. The plug-in will probably not be happy if you change the location of the source files after it is installed (do an uninstall, then a reinstall). See below for a list of known problems. Outlook Addin ========== If you execute "addin.py", the Microsoft Outlook plugin will be installed. Next time outlook is started, you should see a "SpamBayes" drop-down on the toolbar. Clicking it will allow you to maintain your bayes database and filters. All functionality in this package can be accessed from this plugin. This directory contains a number of other files (see below) which can be used to access features of the bayes database and filters from outside of the Outlook environment. Either way, the functionality is the same (except filtering of new mail obviously only works in the Outlook environment) To see any output from the addin (eg, Python print statements) you can either select "Tools->Trace Collector Debugging Tool" from inside Pythonwin, or just execute win32traceutil.py (from the win32all extensions) from a Command Prompt. NOTE: If the addin fails to load, Outlook will automatically disable it for the next time Outlook starts. Re-executing 'addin.py' will ensure the addin is enabled (you can also locate and enable the addin via the labyrinth of Outlook preference dialogs.) If this happens and you have the Python exception that caused the failure (via the tracing mentioned above) please send it to spambayes@python.org. To unregister the addin, execute "addin.py --unregister", then optionally remove the source files. Note that as for the binary version, there is a bug that the toolbar items will remain after an uninstall - see the troubleshooting guide for information on how to restore it. Filtering -------- When running from Outlook, you can enable filtering for all mail that arrives in your Inbox (or any other folder). Note that Outlook's builtin rules will fire before this notification, and if these rules move the message it will never appear in the inbox (and thus will not get spam-filtered by a simple Inbox filter). You can watch as many folders for Spam as you like. Command Line Tools ------------------- There are a number of scripts that invoke the same GUI as the Outlook plugin. manager.py Display the main dialog, which provides access to all other features. train.py Train a classifier from Outlook Mail folders. filter.py Define filters, and allow a bulk-filter to be applied. (The outlook plugin must be running for filtering of new mail to occur) Known Problems --------------- * No field is created in Outlook for the Spam Score field. To create the field, go to the field chooser for the folder you are interested in, and create a new User Property called "Spam". Ensure the type of the field is "Integer" (the last option), NOT "Number". This is only necessary for you to *see* the score, not for the scoring to work. * Sean reports bad output saving very large classifiers in training.py. Somewhere over 4MB, they seem to stop working. Mark's hasn't got that big yet - 3.8 MB, then he moved to the bsddb database - all with no problems. Misc Comments =========== Copyright transferred to PSF from Sean D. True and WebReply.com. Licensed under PSF, see Tim Peters for IANAL interpretation. Copyright transferred to PSF from Mark Hammond. Licensed under PSF, see Tim Peters for IANAL interpretation. Please send all comments, queries, support questions etc to the SpamBayes mailing list - see http://mail.python.org/mailman/listinfo/spambayes -- Sean seant@iname.com -- Mark mhammond@skippinet.com.au spambayes-1.1a6/Outlook2000/sandbox/0000775000076500000240000000000011355064626017265 5ustar skipstaff00000000000000spambayes-1.1a6/Outlook2000/sandbox/delete_outlook_field.py0000664000076500000240000001456210646440135024023 0ustar skipstaff00000000000000from __future__ import generators # Do the best we can to completely obliterate a field from Outlook! from win32com.client import Dispatch, constants import pythoncom import os, sys from win32com.mapi import mapi from win32com.mapi.mapitags import * import mapi_driver def DeleteField_Outlook(folder, name): name = name.lower() entries = folder.Items num_outlook = 0 entry = entries.GetFirst() while entry is not None: up = entry.UserProperties num_props = up.Count for i in range(num_props): if up[i+1].Name.lower()==name: num_outlook += 1 entry.UserProperties.Remove(i+1) entry.Save() break entry = entries.GetNext() return num_outlook def DeleteField_MAPI(driver, folder, name): # OK - now try and wipe the field using MAPI. propIds = folder.GetIDsFromNames(((mapi.PS_PUBLIC_STRINGS,name),), 0) if PROP_TYPE(propIds[0])==PT_ERROR: print "No such field '%s' in folder" % (name,) return 0 assert propIds[0] == PROP_TAG( PT_UNSPECIFIED, PROP_ID(propIds[0])) num_mapi = 0 for item in driver.GetAllItems(folder): # DeleteProps always says"success" - so check to see if it # actually exists just so we can count it. hr, vals = item.GetProps(propIds) if hr==0: # We actually have it hr, probs = item.DeleteProps(propIds) if hr == 0: item.SaveChanges(mapi.MAPI_DEFERRED_ERRORS) num_mapi += 1 return num_mapi def DeleteField_Folder(driver, folder, name): propIds = folder.GetIDsFromNames(((mapi.PS_PUBLIC_STRINGS,name),), 0) if PROP_TYPE(propIds[0])!=PT_ERROR: hr, vals = folder.GetProps(propIds) if hr==0: # We actually have it hr, probs = folder.DeleteProps(propIds) if hr == 0: folder.SaveChanges(mapi.MAPI_DEFERRED_ERRORS) return 1 return 0 def CountFields(folder): fields = {} entries = folder.Items entry = entries.GetFirst() while entry is not None: ups = entry.UserProperties num_props = ups.Count for i in range(num_props): name = ups.Item(i+1).Name fields[name] = fields.get(name, 0)+1 entry = entries.GetNext() for name, num in fields.items(): print name, num def ShowFields(folder, field_name): field_name = field_name.lower() entries = folder.Items entry = entries.GetFirst() while entry is not None: ups = entry.UserProperties num_props = ups.Count for i in range(num_props): up = ups[i+1] name = up.Name if name.lower()==field_name: subject = entry.Subject.encode("mbcs", "replace") print "%s: %s (%d)" % (subject, up.Value, up.Type) entry = entries.GetNext() def usage(driver): folder_doc = driver.GetFolderNameDoc() msg = """\ Usage: %s [-f foldername -f ...] [-d] [-s] [FieldName ...] -f - Run over the specified folders (default = Inbox) -d - Delete the named fields --no-outlook - Don't delete via the Outlook UserProperties API --no-mapi - Don't delete via the extended MAPI API --no-folder - Don't attempt to delete the field from the folder itself -s - Show message subject and field value for all messages with field -n - Show top-level folder names and exit If no options are given, prints a summary of field names in the folders. %s Use the -n option to see all top-level folder names from all stores.""" \ % (os.path.basename(sys.argv[0]), folder_doc) print msg def main(): driver = mapi_driver.MAPIDriver() import getopt try: opts, args = getopt.getopt(sys.argv[1:], "dnsf:", ["no-mapi", "no-outlook", "no-folder"]) except getopt.error, e: print e print usage(driver) sys.exit(1) delete = show = False do_mapi = do_outlook = do_folder = True folder_names = [] for opt, opt_val in opts: if opt == "-d": delete = True elif opt == "-s": show = True elif opt == "-f": folder_names.append(opt_val) elif opt == "--no-mapi": do_mapi = False elif opt == "--no-outlook": do_outlook = False elif opt == "--no-folder": do_folder = False elif opt == "-n": driver.DumpTopLevelFolders() sys.exit(1) else: print "Invalid arg" return if not folder_names: folder_names = ["Inbox"] # Assume this exists! if not args: print "No args specified - dumping all unique UserProperty names," print "and the count of messages they appear in" outlook = None for folder_name in folder_names: try: folder = driver.FindFolder(folder_name) except ValueError, details: print details print "Ignoring folder '%s'" % (folder_name,) continue print "Processing folder '%s'" % (folder_name,) if not args: outlook_folder = driver.GetOutlookFolder(folder) CountFields(outlook_folder) continue for field_name in args: if show: outlook_folder = driver.GetOutlookFolder(folder) ShowFields(outlook_folder, field_name) if delete: print "Deleting field", field_name if do_outlook: outlook_folder = driver.GetOutlookFolder(folder) num = DeleteField_Outlook(outlook_folder, field_name) print "Deleted", num, "field instances from Outlook" if do_mapi: num = DeleteField_MAPI(driver, folder, field_name) print "Deleted", num, "field instances via MAPI" if do_folder: num = DeleteField_Folder(driver, folder, field_name) if num: print "Deleted property from folder" else: print "Could not find property to delete in the folder" ## item = folder.Items.Add() ## props = item.UserProperties ## prop=props.Add("TestInt",3 , True, 1) ## prop.Value=66 ## item.Save() if __name__=='__main__': main() spambayes-1.1a6/Outlook2000/sandbox/dump_email.py0000664000076500000240000000436110646440135021752 0ustar skipstaff00000000000000"""dump one or more items as an 'email object' to stdout.""" import sys, os import optparse from win32com.mapi import mapi, mapiutil from win32com.mapi.mapitags import * import win32clipboard try: from manager import BayesManager except ImportError: if hasattr(sys, "frozen"): raise sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from manager import BayesManager import mapi_driver from cStringIO import StringIO def Dump(driver, manager, mapi_folder, subject, stream=None): for item in driver.GetItemsWithValue(mapi_folder, PR_SUBJECT_A, subject): hr, props = item.GetProps((PR_ENTRYID,PR_STORE_ENTRYID), 0) (tag, eid), (tag, store_eid) = props eid = mapi.HexFromBin(eid) store_eid = mapi.HexFromBin(store_eid) print >> stream, "Dumping message with ID %s/%s" % (store_eid, eid) msm = manager.message_store.GetMessage((store_eid, eid)) ob = msm.GetEmailPackageObject() print >> stream, ob.as_string() print >> stream def main(): driver = mapi_driver.MAPIDriver() parser = optparse.OptionParser("%prog [options] [path ...]", description=__doc__) parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="don't print status messages to stdout") parser.add_option("-f", "--folder", action="store", default="Inbox", help="folder to search") parser.add_option("-c", "--clipboard", action="store", help="write results to the clipboard") options, args = parser.parse_args() subject = " ".join(args) try: folder = driver.FindFolder(options.folder) except ValueError, details: parser.error(details) stream = None if options.clipboard: stream = StringIO() Dump(driver, BayesManager(), folder, subject, stream) if options.clipboard: win32clipboard.OpenClipboard() win32clipboard.EmptyClipboard() win32clipboard.SetClipboardText(stream.getvalue()) print "Output successfuly written to the Windows clipboard" if __name__=='__main__': main() spambayes-1.1a6/Outlook2000/sandbox/dump_profiles.py0000664000076500000240000000145010646440135022502 0ustar skipstaff00000000000000 from win32com.client import Dispatch from win32com.mapi import mapi from win32com.mapi.mapitags import * mapi.MAPIInitialize(None) logonFlags = mapi.MAPI_NO_MAIL | mapi.MAPI_EXTENDED session = mapi.MAPILogonEx(0, None, None, logonFlags) MAPI_SUBSYSTEM = 39 restriction = mapi.RES_PROPERTY, (mapi.RELOP_EQ, PR_RESOURCE_TYPE, (PR_RESOURCE_TYPE,MAPI_SUBSYSTEM)) table = session.GetStatusTable(0) rows = mapi.HrQueryAllRows(table, (PR_DISPLAY_NAME_A,), # columns to retrieve restriction, # only these rows None, # any sort order is fine 0) # any # of results is fine assert len(rows)==1, "Should be exactly one row" (tag, val), = rows[0] print "Profile name:", val spambayes-1.1a6/Outlook2000/sandbox/dump_props.py0000664000076500000240000002331410646440135022025 0ustar skipstaff00000000000000from __future__ import generators # Dump every property we can find for a MAPI item import pythoncom import os, sys import tempfile from win32com.mapi import mapi, mapiutil from win32com.mapi.mapitags import * import win32clipboard import mapi_driver try: TBL_ALL_COLUMNS = mapi.TBL_ALL_COLUMNS except AttributeError: # missing in early versions TBL_ALL_COLUMNS = 1 PR_USERFIELDS = 0x36E30102 # PROP_TAG(PT_BINARY, 0x36e3) def GetPropTagName(obj, prop_tag): hr, tags, array = obj.GetNamesFromIDs( (prop_tag,) ) if type(array[0][1])==type(u''): name = array[0][1] else: name = mapiutil.GetPropTagName(prop_tag) return name # Also in new versions of mapituil def GetAllProperties(obj, make_pretty = True): tags = obj.GetPropList(0) hr, data = obj.GetProps(tags) ret = [] for tag, val in data: if make_pretty: name = GetPropTagName(obj, tag) else: name = tag ret.append((name, tag, val)) return ret def GetLargeProperty(item, prop_tag): prop_tag = PROP_TAG(PT_BINARY, PROP_ID(prop_tag)) stream = item.OpenProperty(prop_tag, pythoncom.IID_IStream, 0, 0) chunks = [] while 1: chunk = stream.Read(4096) if not chunk: break chunks.append(chunk) return "".join(chunks) def FormatPropertyValue(prop_tag, prop_val, item, shorten, get_large_props): # Do some magic rtf conversion if PROP_ID(prop_tag) == PROP_ID(PR_RTF_COMPRESSED): rtf_stream = item.OpenProperty(PR_RTF_COMPRESSED, pythoncom.IID_IStream, 0, 0) html_stream = mapi.WrapCompressedRTFStream(rtf_stream, 0) prop_val = mapi.RTFStreamToHTML(html_stream) prop_tag = PROP_TAG(PT_STRING8, PR_RTF_COMPRESSED) prop_repr = None if PROP_TYPE(prop_tag)==PT_ERROR: if get_large_props and \ prop_val in [mapi.MAPI_E_NOT_ENOUGH_MEMORY, 'MAPI_E_NOT_ENOUGH_MEMORY']: # Use magic to get a large property. prop_val = GetLargeProperty(item, prop_tag) prop_repr = repr(prop_val) else: prop_val = prop_repr = mapiutil.GetScodeString(prop_val) if prop_repr is None: prop_repr = repr(prop_val) if shorten: prop_repr = prop_repr[:50] return prop_repr def DumpItemProps(item, shorten, get_large_props, stream=None): all_props = GetAllProperties(item) all_props.sort() # sort by first tuple item, which is name :) for prop_name, prop_tag, prop_val in all_props: # If we want 'short' variables, drop 'not found' props. if shorten and PROP_TYPE(prop_tag)==PT_ERROR \ and prop_val == mapi.MAPI_E_NOT_FOUND: continue prop_repr = FormatPropertyValue(prop_tag, prop_val, item, shorten, get_large_props) print >> stream, "%-20s: %s" % (prop_name, prop_repr) print >> stream, "-- end of item properties --" def DumpProps(driver, mapi_folder, subject, include_attach, shorten, get_large, stream=None): hr, data = mapi_folder.GetProps( (PR_DISPLAY_NAME_A,), 0) name = data[0][1] for item in driver.GetItemsWithValue(mapi_folder, PR_SUBJECT_A, subject): DumpItemProps(item, shorten, get_large, stream) if include_attach: print >> stream table = item.GetAttachmentTable(0) rows = mapi.HrQueryAllRows(table, (PR_ATTACH_NUM,), None, None, 0) for row in rows: attach_num = row[0][1] print >> stream, \ "Dumping attachment (PR_ATTACH_NUM=%d)" % (attach_num,) attach = item.OpenAttach(attach_num, None, mapi.MAPI_DEFERRED_ERRORS) DumpItemProps(attach, shorten, get_large, stream) print >> stream print >> stream # Generic table dumper. def DumpTable(driver, table, name_query_ob, shorten, large_props, stream=None): cols = table.QueryColumns(TBL_ALL_COLUMNS) table.SetColumns(cols, 0) rows = mapi.HrQueryAllRows(table, cols, None, None, 0) print >> stream, \ "Table has %d rows, each with %d columns" % (len(rows), len(cols)) for row in rows: print >> stream, "-- new row --" for col in row: prop_tag, prop_val = col # If we want 'short' variables, drop 'not found' props. if shorten and PROP_TYPE(prop_tag)==PT_ERROR \ and prop_val == mapi.MAPI_E_NOT_FOUND: continue prop_name = GetPropTagName(name_query_ob, prop_tag) prop_repr = FormatPropertyValue(prop_tag, prop_val, name_query_ob, shorten, large_props) print >> stream, "%-20s: %s" % (prop_name, prop_repr) # This dumps the raw binary data of the property Outlook uses to store # user defined fields. def FindAndDumpTableUserProps(driver, table, folder, shorten, get_large_props, stream=None): restriction = (mapi.RES_PROPERTY, (mapi.RELOP_EQ, PR_MESSAGE_CLASS_A, (PR_MESSAGE_CLASS_A, 'IPC.MS.REN.USERFIELDS'))) cols = (PR_USERFIELDS,) table.SetColumns(cols, 0) rows = mapi.HrQueryAllRows(table, cols, restriction, None, 0) assert len(rows)<=1, "Only expecting 1 (or 0) rows" tag, val = rows[0][0] prop_name = GetPropTagName(folder, tag) prop_repr = FormatPropertyValue(tag, val, folder, shorten, get_large_props) print >> stream, "%-20s: %s" % (prop_name, prop_repr) def usage(driver, extra = None): folder_doc = driver.GetFolderNameDoc() if extra: print extra print msg = """\ Usage: %s [options ...] subject of the message Dumps all properties for all messages that match the subject. Subject matching is substring and ignore-case. -c - Write output to the clipboard, ready for pasting into an email -f - Search for the message in the specified folder (default = Inbox) -s - Shorten long property values. -a - Include attachments -l - Get the data for very large properties via a stream -n - Show top-level folder names and exit --dump-folder Dump the properties of the specified folder. --dump-folder-assoc-contents Dump the 'associated contents' table of the specified folder. --dump-folder-user-props Find and dump the PR_USERFIELDS field for the specified table. %s Use the -n option to see all top-level folder names from all stores.""" \ % (os.path.basename(sys.argv[0]),folder_doc) print msg sys.exit(1) def main(): driver = mapi_driver.MAPIDriver() import getopt try: opts, args = getopt.getopt(sys.argv[1:], "caf:snl", ["dump-folder", "dump-folder-assoc-contents", "dump-folder-user-props", ]) except getopt.error, e: usage(driver, e) folder_name = "" shorten = False get_large_props = False include_attach = False write_clipboard = False dump_folder = dump_folder_assoc_contents = dump_folder_user_props = False for opt, opt_val in opts: if opt == "-f": folder_name = opt_val elif opt == "-c": write_clipboard = True elif opt == "--dump-folder": dump_folder = True elif opt == "--dump-folder-assoc-contents": dump_folder_assoc_contents = True elif opt == "--dump-folder-user-props": dump_folder_user_props = True elif opt == "-s": shorten = True elif opt == "-a": include_attach = True elif opt == "-l": get_large_props = True elif opt == "-n": driver.DumpTopLevelFolders() sys.exit(1) else: usage(driver, "Unknown arg '%s'" % opt) stream = None if write_clipboard: stream_name = tempfile.mktemp("spambayes") stream = open(stream_name, "w") if not folder_name: folder_name = "Inbox" # Assume this exists! subject = " ".join(args) is_table_dump = dump_folder_assoc_contents or \ dump_folder or dump_folder_user_props if is_table_dump and subject or not is_table_dump and not subject: if is_table_dump: extra = "You must not specify a subject with '-p'" else: extra = "You must specify a subject (unless you use '-p')" usage(driver, extra) try: folder = driver.FindFolder(folder_name) except ValueError, details: print details sys.exit(1) if is_table_dump: if dump_folder: DumpItemProps(folder, shorten, get_large_props, stream) if dump_folder_assoc_contents: table = folder.GetContentsTable(mapi.MAPI_ASSOCIATED) DumpTable(driver, table, folder, shorten, get_large_props, stream) if dump_folder_user_props: table = folder.GetContentsTable(mapi.MAPI_ASSOCIATED) FindAndDumpTableUserProps(driver, table, folder, shorten, get_large_props, stream) else: DumpProps(driver, folder, subject, include_attach, shorten, get_large_props, stream) if write_clipboard: stream.close() stream = open(stream_name, "r") win32clipboard.OpenClipboard() win32clipboard.EmptyClipboard() win32clipboard.SetClipboardText(stream.read()) stream.close() os.unlink(stream_name) print "Output successfuly written to the Windows clipboard" if __name__=='__main__': main() spambayes-1.1a6/Outlook2000/sandbox/extract_bad_msg_from_log.py0000664000076500000240000000135410646440135024647 0ustar skipstaff00000000000000# extract_bad_msg_from_log.py import sys def main(argv = None): if argv is None: argv = sys.argv if len(argv) < 2: print "Need log filename" return f = open(argv[1]) trigger = "FAILED to create email.message from: " quotes = "'\"" for line in f: if line.startswith(trigger): msg_repr = line[len(trigger):] if msg_repr[0] not in quotes or msg_repr[-2] not in quotes: print "eeek - not a string repr!" return msg_str = eval(msg_repr) # damn it - stderr in text mode msg_str = msg_str.replace("\r\n", "\n") sys.stdout.write(msg_str) inname = sys.argv[1] if __name__=='__main__': main() spambayes-1.1a6/Outlook2000/sandbox/extract_prop.py0000664000076500000240000001031710646440135022346 0ustar skipstaff00000000000000# Extract a property from a message to a file. This is about the only # way to extract huge properties such that the original data is available. import sys, os import mapi_driver from win32com.mapi import mapitags, mapi import pythoncom def DumpItemProp(item, prop, outfile): if type(prop)!=type(0): # see if a mapitags contant try: prop = mapitags.__dict__[prop] except KeyError: # resolve as a name props = ( (mapi.PS_PUBLIC_STRINGS, prop), ) propIds = obj.GetIDsFromNames(props, 0) prop = mapitags.PROP_TAG( mapitags.PT_UNSPECIFIED, mapitags.PROP_ID(propIds[0])) hr, data = item.GetProps((prop,), 0) prop_tag, prop_val = data[0] # Do some magic rtf conversion if mapitags.PROP_ID(prop_tag) == mapitags.PROP_ID(mapitags.PR_RTF_COMPRESSED): rtf_stream = item.OpenProperty(mapitags.PR_RTF_COMPRESSED, pythoncom.IID_IStream, 0, 0) html_stream = mapi.WrapCompressedRTFStream(rtf_stream, 0) chunks = [] while 1: chunk = html_stream.Read(4096) if not chunk: break chunks.append(chunk) prop_val = "".join(chunks) elif mapitags.PROP_TYPE(prop_tag)==mapitags.PT_ERROR and \ prop_val in [mapi.MAPI_E_NOT_ENOUGH_MEMORY,'MAPI_E_NOT_ENOUGH_MEMORY']: prop_tag = mapitags.PROP_TAG(mapitags.PT_BINARY, mapitags.PROP_ID(prop_tag)) stream = item.OpenProperty(prop_tag, pythoncom.IID_IStream, 0, 0) chunks = [] while 1: chunk = stream.Read(4096) if not chunk: break chunks.append(chunk) prop_val = "".join(chunks) outfile.write(prop_val) def DumpProp(driver, mapi_folder, subject, prop_tag, outfile): hr, data = mapi_folder.GetProps( (mapitags.PR_DISPLAY_NAME_A,), 0) name = data[0][1] items = driver.GetItemsWithValue(mapi_folder, mapitags.PR_SUBJECT_A, subject) num = 0 for item in items: if num > 1: print >> sys.stderr, "Warning: More than one matching item - ignoring" break DumpItemProp(item, prop_tag, outfile) num += 1 if num==0: print >> sys.stderr, "Error: No matching items" def usage(driver): folder_doc = driver.GetFolderNameDoc() msg = """\ Usage: %s [-f foldername] [-o output_file] -p property_name subject of the message -f - Search for the message in the specified folder (default = Inbox) -p - Name of the property to dump -o - Output file to be created - default - stdout. Dumps all properties for all messages that match the subject. Subject matching is substring and ignore-case. %s Use the -n option to see all top-level folder names from all stores.""" \ % (os.path.basename(sys.argv[0]),folder_doc) print msg def main(): driver = mapi_driver.MAPIDriver() import getopt try: opts, args = getopt.getopt(sys.argv[1:], "np:f:o:") except getopt.error, e: print e print usage(driver) sys.exit(1) folder_name = prop_name = output_name = "" for opt, opt_val in opts: if opt == "-p": prop_name = opt_val elif opt == "-f": folder_name = opt_val elif opt == '-o': output_name = os.path.abspath(opt_val) elif opt == "-n": driver.DumpTopLevelFolders() sys.exit(1) else: print "Invalid arg" return if not folder_name: folder_name = "Inbox" # Assume this exists! subject = " ".join(args) if not subject: print "You must specify a subject" print usage(driver) sys.exit(1) if not prop_name: print "You must specify a property" print usage(driver) sys.exit(1) if output_name: output_file = file(output_name, "wb") else: output_file = sys.stdout try: folder = driver.FindFolder(folder_name) except ValueError, details: print details sys.exit(1) DumpProp(driver, folder, subject, prop_name, output_file) if __name__=='__main__': main() spambayes-1.1a6/Outlook2000/sandbox/find_dupe_props.py0000664000076500000240000000634510646440135023022 0ustar skipstaff00000000000000from __future__ import generators # Dump every property we can find for a MAPI item import pythoncom import os, sys from win32com.mapi import mapi, mapiutil from win32com.mapi.mapitags import * import mapi_driver def FindDupeProps(driver, mapi_folder, prop_tag, dupe_dict): hr, data = mapi_folder.GetProps( (PR_DISPLAY_NAME_A,), 0) name = data[0][1] try: prop_tag = int(prop_tag) except ValueError: # See if a constant in mapitags. if prop_tag.startswith("PR_") and prop_tag in globals(): prop_tag = globals()[prop_tag] else: props = ( (mapi.PS_PUBLIC_STRINGS, prop_tag), ) ids = mapi_folder.GetIDsFromNames(props, 0) if PROP_ID(ids[0])==0: print "Could not resolve property '%s'" % prop_tag return 1 prop_tag = PROP_TAG( PT_UNSPECIFIED, PROP_ID(ids[0])) num_with_prop = num_without_prop = 0 for item in driver.GetAllItems(mapi_folder): hr, data = item.GetProps( (prop_tag,PR_SUBJECT_A, PR_ENTRYID), 0) if hr==0: (tag_hr, tag_data) = data[0] (subject_hr, subject_data) = data[1] (eid_hr, eid_data) = data[2] dupe_dict.setdefault(tag_data, []).append((eid_data, subject_data)) num_with_prop += 1 else: num_without_prop += 1 print "Folder '%s': %d items with the property and %d items without it" \ % (name, num_with_prop, num_without_prop) def DumpDupes(dupe_dict): for val, items in dupe_dict.items(): if len(items)>1: print "Found %d items with property value %r" % (len(items), val) for (eid, subject) in items: print "", subject def usage(driver): folder_doc = driver.GetFolderNameDoc() msg = """\ Usage: %s [-f foldername] [-f ...] property_name_or_tag -f - Search for the message in the specified folders (default = Inbox) -n - Show top-level folder names and exit Dumps all properties for all messages that match the subject. Subject matching is substring and ignore-case. %s Use the -n option to see all top-level folder names from all stores.""" \ % (os.path.basename(sys.argv[0]),folder_doc) print msg def main(): driver = mapi_driver.MAPIDriver() import getopt try: opts, args = getopt.getopt(sys.argv[1:], "f:n") except getopt.error, e: print e print usage(driver) sys.exit(1) folder_names = [] for opt, opt_val in opts: if opt == "-f": folder_names.append(opt_val) elif opt == "-n": driver.DumpTopLevelFolders() sys.exit(1) else: print "Invalid arg" return if not folder_names: folder_names = ["Inbox"] # Assume this exists! if len(args) != 1: print "You must specify a property tag/name" print usage(driver) sys.exit(1) dupe_dict = {} for folder_name in folder_names: try: folder = driver.FindFolder(folder_name) except ValueError, details: print details sys.exit(1) FindDupeProps(driver, folder, args[0], dupe_dict) DumpDupes(dupe_dict) if __name__=='__main__': main() spambayes-1.1a6/Outlook2000/sandbox/mapi_driver.py0000664000076500000240000001677310646440135022151 0ustar skipstaff00000000000000from __future__ import generators # Utilities for our sandbox import os import pythoncom from win32com.mapi import mapi, mapiutil from win32com.mapi.mapitags import * from win32com.client import Dispatch class MAPIDriver: def __init__(self, read_only = False): old_cwd = os.getcwd() mapi.MAPIInitialize(None) logonFlags = (mapi.MAPI_NO_MAIL | mapi.MAPI_EXTENDED | mapi.MAPI_USE_DEFAULT) self.session = mapi.MAPILogonEx(0, None, None, logonFlags) if read_only: self.mapi_flags = mapi.MAPI_DEFERRED_ERRORS else: self.mapi_flags = mapi.MAPI_DEFERRED_ERRORS | mapi.MAPI_BEST_ACCESS self.outlook = None os.chdir(old_cwd) def _GetMAPIFlags(self, mapi_flags = None): if mapi_flags is None: mapi_flags = self.mapi_flags return mapi_flags def GetOutlookFolder(self, item): if self.outlook is None: self.outlook = Dispatch("Outlook.Application") hr, props = item.GetProps((PR_ENTRYID,PR_STORE_ENTRYID), 0) (tag, eid), (tag, store_eid) = props eid = mapi.HexFromBin(eid) store_eid = mapi.HexFromBin(store_eid) return self.outlook.Session.GetFolderFromID(eid, store_eid) def GetMessageStores(self): tab = self.session.GetMsgStoresTable(0) rows = mapi.HrQueryAllRows(tab, (PR_ENTRYID, PR_DISPLAY_NAME_A, PR_DEFAULT_STORE), # columns to retrieve None, # all rows None, # any sort order is fine 0) # any # of results is fine for row in rows: (eid_tag, eid), (name_tag, name), (def_store_tag, def_store) = row # Open the store. try: store = self.session.OpenMsgStore( 0, # no parent window eid, # msg store to open None, # IID; accept default IMsgStore # need write access to add score fields mapi.MDB_WRITE | # we won't send or receive email mapi.MDB_NO_MAIL | mapi.MAPI_DEFERRED_ERRORS) yield store, name, def_store except pythoncom.com_error, details: hr, msg, exc, arg_err = details if hr== mapi.MAPI_E_FAILONEPROVIDER: # not logged on etc. pass else: print "Error opening message store", details, "- ignoring" def _FindSubfolder(self, store, folder, find_name): find_name = find_name.lower() table = folder.GetHierarchyTable(0) rows = mapi.HrQueryAllRows(table, (PR_ENTRYID, PR_DISPLAY_NAME_A), None, None, 0) for (eid_tag, eid), (name_tag, name), in rows: if name.lower() == find_name: return store.OpenEntry(eid, None, mapi.MAPI_DEFERRED_ERRORS) return None def FindFolder(self, name): assert name names = [n.lower() for n in name.split("\\")] if names[0]: store_name = None for store, name, is_default in self.GetMessageStores(): if is_default: store_name = name.lower() break if store_name is None: raise RuntimeError, "Can't find a default message store" folder_names = names else: store_name = names[1] folder_names = names[2:] # Find the store with the name for store, name, is_default in self.GetMessageStores(): if name.lower() == store_name: folder_store = store break else: raise ValueError, "The store '%s' can not be located" % (store_name,) hr, data = store.GetProps((PR_IPM_SUBTREE_ENTRYID,), 0) subtree_eid = data[0][1] folder = folder_store.OpenEntry(subtree_eid, None, mapi.MAPI_DEFERRED_ERRORS) for name in folder_names: folder = self._FindSubfolder(folder_store, folder, name) if folder is None: raise ValueError, "The subfolder '%s' can not be located" % (name,) return folder def GetAllItems(self, folder, mapi_flags = None): mapi_flags = self._GetMAPIFlags(mapi_flags) table = folder.GetContentsTable(0) table.SetColumns((PR_ENTRYID,PR_STORE_ENTRYID), 0) while 1: # Getting 70 at a time was the random number that gave best # perf for me ;) rows = table.QueryRows(70, 0) if len(rows) == 0: break for row in rows: (tag, eid), (tag, store_eid) = row store = self.session.OpenMsgStore(0, store_eid, None, mapi_flags) item = store.OpenEntry(eid, None, mapi_flags) yield item def GetItemsWithValue(self, folder, prop_tag, prop_val, mapi_flags = None): mapi_flags = self._GetMAPIFlags(mapi_flags) tab = folder.GetContentsTable(0) # Restriction for the table: get rows where our prop values match restriction = (mapi.RES_CONTENT, # a property restriction (mapi.FL_SUBSTRING | mapi.FL_IGNORECASE | mapi.FL_LOOSE, # fuzz level prop_tag, # of the given prop (prop_tag, prop_val))) # with given val rows = mapi.HrQueryAllRows(tab, (PR_ENTRYID, PR_STORE_ENTRYID), # columns to retrieve restriction, # only these rows None, # any sort order is fine 0) # any # of results is fine for row in rows: (tag, eid),(tag, store_eid) = row store = self.session.OpenMsgStore(0, store_eid, None, mapi_flags) item = store.OpenEntry(eid, None, mapi_flags) yield item def DumpTopLevelFolders(self): print "Top-level folder names are:" for store, name, is_default in self.GetMessageStores(): # Find the folder with the content. hr, data = store.GetProps((PR_IPM_SUBTREE_ENTRYID,), 0) subtree_eid = data[0][1] folder = store.OpenEntry(subtree_eid, None, mapi.MAPI_DEFERRED_ERRORS) # Now the top-level folders in the store. table = folder.GetHierarchyTable(0) rows = mapi.HrQueryAllRows(table, (PR_DISPLAY_NAME_A), None, None, 0) for (name_tag, folder_name), in rows: print " \\%s\\%s" % (name, folder_name) def GetFolderNameDoc(self): def_store_name = "" for store, name, is_def in self.GetMessageStores(): if is_def: def_store_name = name return """\ Folder name is a hierarchical 'path' name, using '\\' as the path separator. If the folder name begins with a \\, it must be a fully-qualified name, including the message store name. For example, as your default store is currently named '%s', your Inbox can be specified either as: -f "Inbox" or -f "\\%s\\Inbox" """ % (def_store_name, def_store_name) if __name__=='__main__': print "This is a utility script for the other scripts in this directory" spambayes-1.1a6/Outlook2000/sandbox/score.py0000664000076500000240000001016010646440135020743 0ustar skipstaff00000000000000"""Scores one or more items in your Outlook store.""" # score one or more items, write results to stdout. # Helps test new features (eg, OCR) outside the Outlook environment. import sys, os import optparse from win32com.mapi import mapi, mapiutil from win32com.mapi.mapitags import * import win32clipboard try: from manager import BayesManager except ImportError: if hasattr(sys, "frozen"): raise sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from manager import BayesManager from addin import GetClues import mapi_driver from cStringIO import StringIO def Score(driver, manager, mapi_folder, subject, options, stream=None): num = 0 if options.all: getter = driver.GetAllItems getter_args = (mapi_folder,) else: getter = driver.GetItemsWithValue getter_args = (mapi_folder, PR_SUBJECT_A, subject) for item in getter(*getter_args): num += 1 if num % 1000 == 0: print >> sys.stderr, "Processed", num, "items..." hr, props = item.GetProps((PR_ENTRYID,PR_STORE_ENTRYID, PR_SUBJECT_A), 0) (tag, eid), (tag, store_eid), (tag, sub) = props eid = mapi.HexFromBin(eid) store_eid = mapi.HexFromBin(store_eid) try: msm = manager.message_store.GetMessage((store_eid, eid)) manager.classifier_data.message_db.load_msg(msm) score = manager.score(msm) if not options.quiet: print "Message %r scored %g" % (sub, score) if options.show_clues: clues = GetClues(manager, msm) if not options.quiet: print >> stream, clues if options.quiet: continue if options.show_image_info: eob = msm.GetEmailPackageObject() # Show what the OCR managed to extract. from spambayes.ImageStripper import crack_images from spambayes.tokenizer import imageparts image_text, image_toks = crack_images(imageparts(eob)) print >> stream, "Image text:", repr(image_text) print >> stream, "Image tokens:", repr(image_toks) print >> stream # blank lines between messages except: print >> sys.stderr, "FAILED to convert message:", sub raise print >> stream, "Scored", num, "messages." def main(): driver = mapi_driver.MAPIDriver() parser = optparse.OptionParser("%prog [options] subject of message ...", description=__doc__) parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="don't print score info - useful for testing") parser.add_option("-f", "--folder", action="store", default="Inbox", help="folder to search") parser.add_option("", "--clipboard", action="store_true", help="write results to the clipboard") parser.add_option("-c", "--show-clues", action="store_true", help="also write the clues for the message") parser.add_option("-a", "--all", action="store_true", help="ignore the subject and score all items in the folder") parser.add_option("-i", "--show-image-info", action="store_true", help="show the information we can extract from images " "in the mail") options, args = parser.parse_args() subject = " ".join(args) try: folder = driver.FindFolder(options.folder) except ValueError, details: parser.error(details) stream = None if options.clipboard: stream = StringIO() Score(driver, BayesManager(), folder, subject, options, stream) if options.clipboard: win32clipboard.OpenClipboard() win32clipboard.EmptyClipboard() win32clipboard.SetClipboardText(stream.getvalue()) print "Output successfuly written to the Windows clipboard" if __name__=='__main__': main() spambayes-1.1a6/Outlook2000/sandbox/set_read_flag.py0000664000076500000240000000472310646440135022417 0ustar skipstaff00000000000000from __future__ import generators # Set items to read/unread import pythoncom import os, sys from win32com.mapi import mapi, mapiutil from win32com.mapi.mapitags import * MSGFLAG_READ = 0x1 CLEAR_READ_FLAG = 0x00000004 CLEAR_RN_PENDING = 0x00000020 CLEAR_NRN_PENDING = 0x00000040 SUPPRESS_RECEIPT = 0x1 import mapi_driver def SetReadState(driver, mapi_folder, subject, unread): hr, data = mapi_folder.GetProps( (PR_DISPLAY_NAME_A,), 0) name = data[0][1] num = 0 for item in driver.GetItemsWithValue(mapi_folder, PR_SUBJECT_A, subject): flags_base = mapi.MAPI_DEFERRED_ERRORS | SUPPRESS_RECEIPT if unread: item.SetReadFlag(mapi.MAPI_DEFERRED_ERRORS|CLEAR_READ_FLAG) else: item.SetReadFlag(flags_base) num += 1 # Check the set worked. hr, props = item.GetProps((PR_MESSAGE_FLAGS,), 0) ((tag, val), ) = props if val & MSGFLAG_READ == unread: print "MAPI SetReadState appears to have failed to change the message state" print "Requested set to unread=%s but the MAPI field after was %r" % \ (unread, val) print "Processed", num, "items" def usage(driver): folder_doc = driver.GetFolderNameDoc() msg = """\ Usage: %s [-u] subject of the message -f - Search for the message in the specified folder (default = Inbox) -u - Mark as unread Marks as read (or unread) all messages that match the subject. Subject matching is substring and ignore-case. %s Use the -n option to see all top-level folder names from all stores.""" \ % (os.path.basename(sys.argv[0]),folder_doc) print msg def main(): driver = mapi_driver.MAPIDriver() import getopt try: opts, args = getopt.getopt(sys.argv[1:], "u") except getopt.error, e: print e print usage(driver) sys.exit(1) folder_name = "" unread = False for opt, opt_val in opts: if opt == "-u": unread = True else: print "Invalid arg" return if not folder_name: folder_name = "Inbox" # Assume this exists! subject = " ".join(args) if not subject: print "You must specify a subject" print usage(driver) sys.exit(1) try: folder = driver.FindFolder(folder_name) except ValueError, details: print details sys.exit(1) SetReadState(driver, folder, subject, unread) if __name__=='__main__': main() spambayes-1.1a6/Outlook2000/tester.py0000664000076500000240000007205410646440136017513 0ustar skipstaff00000000000000# unit tester for the Outlook addin. # # Note we are only attempting to test Outlook specific # functionality, such as filters, etc. # # General process is to create test messages known to contain ham/spam # keywords, and tracking their progress through the filters. We also # move this test message back around, and watch the incremental retrain # in action. Also checks that the message correctly remains classified # after a message move. from __future__ import generators from win32com.client import constants import sys from time import sleep import copy import rfc822 import cStringIO import threading from spambayes.storage import STATE_KEY import msgstore from win32com.mapi import mapi, mapiutil import pythoncom HAM="ham" SPAM="spam" UNSURE="unsure" TEST_SUBJECT = "SpamBayes addin auto-generated test message" class TestFailure(Exception): pass def TestFailed(msg): raise TestFailure(msg) def AssertRaises(exception, func, *args): try: func(*args) raise TestFailed("Function '%s' should have raised '%r', but it worked!" % \ (func, exception)) except: exc_type = sys.exc_info()[0] if exc_type == exception or issubclass(exc_type, exception): return raise filter_event = threading.Event() def WaitForFilters(): # Must wait longer than normal, so when run with a timer we still work. filter_event.clear() for i in range(500): pythoncom.PumpWaitingMessages() if filter_event.isSet(): break sleep(0.01) def DictExtractor(bayes): for k, v in bayes.wordinfo.items(): yield k, v def DBExtractor(bayes): # We use bsddb3 now if we can try: import bsddb3 as bsddb bsddb_error = bsddb.db.DBNotFoundError except ImportError: import bsddb bsddb_error = bsddb.error key = bayes.dbm.first()[0] if key != STATE_KEY: yield key, bayes._wordinfoget(key) while True: try: key = bayes.dbm.next()[0] except bsddb.error: break except bsddb_error: break if key != STATE_KEY: yield key, bayes._wordinfoget(key) # Find the top 'n' words in the Spam database that are clearly # marked as either ham or spam. Simply enumerates the # bayes word list looking for any word with zero count in the # non-requested category. _top_ham = None _top_spam = None def FindTopWords(bayes, num, get_spam): global _top_spam, _top_ham if get_spam and _top_spam: return _top_spam if not get_spam and _top_ham: return _top_ham items = [] try: bayes.db # bsddb style extractor = DBExtractor except AttributeError: extractor = DictExtractor for word, info in extractor(bayes): if info is None: break if ":" in word: continue if get_spam: if info.hamcount==0: items.append((info.spamcount, word, info)) else: if info.spamcount==0: items.append((info.hamcount, word, info)) items.sort() items.reverse() # Throw an error if we don't have enough tokens - otherwise # the test itself may fail, which will be more confusing than this. if len(items) < num: TestFailed("Error: could not find %d words with Spam=%s - only found %d" % (num, get_spam, len(items))) ret = {} for n, word, info in items[:num]: ret[word]=copy.copy(info) if get_spam: _top_spam = ret else: _top_ham = ret return ret # A little driver/manager for our tests class Driver: def __init__(self, mgr): if mgr is None: import manager mgr = manager.GetManager() self.manager = mgr # Remember the "spam" folder. folder = mgr.message_store.GetFolder(mgr.config.filter.spam_folder_id) self.folder_spam = folder.GetOutlookItem() # Remember the "unsure" folder. folder = mgr.message_store.GetFolder(mgr.config.filter.unsure_folder_id) self.folder_unsure = folder.GetOutlookItem() # And the drafts folder where new messages are created. self.folder_drafts = mgr.outlook.Session.GetDefaultFolder(constants.olFolderDrafts) def GetWatchFolderGenerator(self): mgr = self.manager gen = mgr.message_store.GetFolderGenerator( mgr.config.filter.watch_folder_ids, mgr.config.filter.watch_include_sub) for f in gen: yield f, f.GetOutlookItem() def FindTestMessage(self, folder): subject = TEST_SUBJECT items = folder.Items return items.Find("[Subject] = '%s'" % (subject,)) def CheckMessageFilteredFrom(self, folder): # For hotmail accounts, the message may take a little time to actually # be removed from the original folder (ie, it appears in the "dest" # folder before it vanished. for i in range(5): if self.FindTestMessage(folder) is None: break for j in range(10): sleep(.05) else: ms_folder = self.manager.message_store.GetFolder(folder) TestFailed("The test message remained in folder '%s'" % ms_folder.GetFQName()) def _CleanTestMessageFromFolder(self, folder): subject = TEST_SUBJECT num = 0 # imap/hotmail etc only soft delete, and I see no way to differentiate # force the user to purge them manually for i in range(50): msg = self.FindTestMessage(folder) if msg is None: break msg.Delete() else: raise TestFailed("Old test messages appear to still exist. These may" \ "be 'soft-deleted' - you will need to purge them manually") if num: print "Cleaned %d test messages from folder '%s'" % (num, folder.Name) def CleanAllTestMessages(self): self._CleanTestMessageFromFolder(self.folder_spam) self._CleanTestMessageFromFolder(self.folder_unsure) self._CleanTestMessageFromFolder(self.folder_drafts) for msf, of in self.GetWatchFolderGenerator(): self._CleanTestMessageFromFolder(of) def CreateTestMessageInFolder(self, spam_status, folder): msg, words = self.CreateTestMessage(spam_status) msg.Save() # Put into "Drafts". assert self.FindTestMessage(self.folder_drafts) is not None # Move it to the specified folder msg.Move(folder) # And now find it in the specified folder return self.FindTestMessage(folder), words def CreateTestMessage(self, spam_status): words = {} bayes = self.manager.classifier_data.bayes if spam_status != SPAM: words.update(FindTopWords(bayes, 50, False)) if spam_status != HAM: words.update(FindTopWords(bayes, 50, True)) # Create a new blank message with our words msg = self.manager.outlook.CreateItem(0) msg.Body = "\n".join(words.keys()) msg.Subject = TEST_SUBJECT return msg, words def check_words(words, bayes, spam_offset, ham_offset): for word, existing_info in words.items(): new_info = bayes._wordinfoget(word) if existing_info.spamcount+spam_offset != new_info.spamcount or \ existing_info.hamcount+ham_offset != new_info.hamcount: TestFailed("Word check for '%s failed. " "old spam/ham=%d/%d, new spam/ham=%d/%d," "spam_offset=%d, ham_offset=%d" % \ (word, existing_info.spamcount, existing_info.hamcount, new_info.spamcount, new_info.hamcount, spam_offset, ham_offset)) # The tests themselves. # The "spam" test is huge - we do standard filter tests, but # also do incremental retrain tests. def TestSpamFilter(driver): bayes = driver.manager.classifier_data.bayes nspam = bayes.nspam nham = bayes.nham original_bayes = copy.copy(driver.manager.classifier_data.bayes) # for each watch folder, create a spam message, and do the training thang for msf_watch, folder_watch in driver.GetWatchFolderGenerator(): print "Performing Spam test on watch folder '%s'..." % msf_watch.GetFQName() # Create a spam message in the Inbox - it should get immediately filtered msg, words = driver.CreateTestMessageInFolder(SPAM, folder_watch) # sleep to ensure filtering. WaitForFilters() # It should no longer be in the Inbox. driver.CheckMessageFilteredFrom(folder_watch) # It should be in the "sure is spam" folder. spam_msg = driver.FindTestMessage(driver.folder_spam) if spam_msg is None: TestFailed("The test message vanished from the Inbox, but didn't appear in Spam") # Check that none of the above caused training. if nspam != bayes.nspam: TestFailed("Something caused a new spam message to appear") if nham != bayes.nham: TestFailed("Something caused a new ham message to appear") check_words(words, bayes, 0, 0) # Now move the message back to the inbox - it should get trained. store_msg = driver.manager.message_store.GetMessage(spam_msg) driver.manager.classifier_data.message_db.load_msg(store_msg) import train if train.been_trained_as_ham(store_msg): TestFailed("This new spam message should not have been trained as ham yet") if train.been_trained_as_spam(store_msg): TestFailed("This new spam message should not have been trained as spam yet") spam_msg.Move(folder_watch) WaitForFilters() spam_msg = driver.FindTestMessage(folder_watch) if spam_msg is None: TestFailed("The message appears to have been filtered out of the watch folder") store_msg = driver.manager.message_store.GetMessage(spam_msg) driver.manager.classifier_data.message_db.load_msg(store_msg) need_untrain = True try: if nspam != bayes.nspam: TestFailed("There were not the same number of spam messages after a re-train") if nham+1 != bayes.nham: TestFailed("There was not one more ham messages after a re-train") if train.been_trained_as_spam(store_msg): TestFailed("This new spam message should not have been trained as spam yet") if not train.been_trained_as_ham(store_msg): TestFailed("This new spam message should have been trained as ham now") # word infos should have one extra ham check_words(words, bayes, 0, 1) # Now move it back to the Spam folder. # This should see the message un-trained as ham, and re-trained as Spam spam_msg.Move(driver.folder_spam) WaitForFilters() spam_msg = driver.FindTestMessage(driver.folder_spam) if spam_msg is None: TestFailed("Could not find the message in the Spam folder") store_msg = driver.manager.message_store.GetMessage(spam_msg) driver.manager.classifier_data.message_db.load_msg(store_msg) if nspam +1 != bayes.nspam: TestFailed("There should be one more spam now") if nham != bayes.nham: TestFailed("There should be the same number of hams again") if not train.been_trained_as_spam(store_msg): TestFailed("This new spam message should have been trained as spam by now") if train.been_trained_as_ham(store_msg): TestFailed("This new spam message should have been un-trained as ham") # word infos should have one extra spam, no extra ham check_words(words, bayes, 1, 0) # Move the message to another folder, and make sure we still # identify it correctly as having been trained. # Move to the "unsure" folder, just cos we know about it, and # we know that no special watching of this folder exists. spam_msg.Move(driver.folder_unsure) spam_msg = driver.FindTestMessage(driver.folder_unsure) if spam_msg is None: TestFailed("Could not find the message in the Unsure folder") store_msg = driver.manager.message_store.GetMessage(spam_msg) driver.manager.classifier_data.message_db.load_msg(store_msg) if not train.been_trained_as_spam(store_msg): TestFailed("Message was not identified as Spam after moving") # word infos still be 'spam' check_words(words, bayes, 1, 0) # Now undo the damage we did. was_spam = train.untrain_message(store_msg, driver.manager.classifier_data) driver.manager.classifier_data.message_db.load_msg(store_msg) if not was_spam: TestFailed("Untraining this message did not indicate it was spam") if train.been_trained_as_spam(store_msg) or \ train.been_trained_as_ham(store_msg): TestFailed("Untraining this message kept it has ham/spam") need_untrain = False finally: if need_untrain: train.untrain_message(store_msg, driver.manager.classifier_data) # Check all the counts are back where we started. if nspam != bayes.nspam: TestFailed("Spam count didn't get back to the same") if nham != bayes.nham: TestFailed("Ham count didn't get back to the same") check_words(words, bayes, 0, 0) if bayes.wordinfo != original_bayes.wordinfo: TestFailed("The bayes object's 'wordinfo' did not compare the same at the end of all this!") if bayes.probcache != original_bayes.probcache: TestFailed("The bayes object's 'probcache' did not compare the same at the end of all this!") spam_msg.Delete() print "Created a Spam message, and saw it get filtered and trained." def _DoTestHamTrain(driver, folder1, folder2): # [ 780612 ] Outlook incorrectly trains on moved messages # Should not train when previously classified message is moved by the user # from one watch folder to another. bayes = driver.manager.classifier_data.bayes nham = bayes.nham nspam = bayes.nspam # Create a ham message in the Inbox - it wont get filtered if the other # tests pass, but we do need to wait for it to be scored. msg, words = driver.CreateTestMessageInFolder(HAM, folder1) # sleep to ensure filtering. WaitForFilters() # It should still be in the Inbox. if driver.FindTestMessage(folder1) is None: TestFailed("The test ham message appeared to have been filtered!") # Manually move it to folder2 msg.Move(folder2) # sleep to any processing in this folder. WaitForFilters() # re-find it in folder2 msg = driver.FindTestMessage(folder2) if driver.FindTestMessage(folder2) is None: TestFailed("Couldn't find the ham message we just moved") if nspam != bayes.nspam or nham != bayes.nham: TestFailed("Move of existing ham caused a train") msg.Delete() def _DoTestHamFilter(driver, folder): # Create a ham message in the Inbox - it should not get filtered msg, words = driver.CreateTestMessageInFolder(HAM, folder) # sleep to ensure filtering. WaitForFilters() # It should still be in the Inbox. if driver.FindTestMessage(folder) is None: TestFailed("The test ham message appeared to have been filtered!") msg.Delete() def TestHamFilter(driver): # Execute the 'ham' test in every folder we watch. mgr = driver.manager gen = mgr.message_store.GetFolderGenerator( mgr.config.filter.watch_folder_ids, mgr.config.filter.watch_include_sub) num = 0 folders = [] for f in gen: print "Running ham filter tests on folder '%s'" % f.GetFQName() f = f.GetOutlookItem() _DoTestHamFilter(driver, f) num += 1 folders.append(f) # Now test incremental train logic, between all these folders. if len(folders)<2: print "NOTE: Can't do incremental training tests as only 1 watch folder is in place" else: for f in folders: # 'targets' is a list of all folders except this targets = folders[:] targets.remove(f) for t in targets: _DoTestHamTrain(driver, f, t) print "Created a Ham message, and saw it remain in place (in %d watch folders.)" % num def TestUnsureFilter(driver): # Create a spam message in the Inbox - it should get immediately filtered for msf_watch, folder_watch in driver.GetWatchFolderGenerator(): print "Performing Spam test on watch folder '%s'..." % msf_watch.GetFQName() msg, words = driver.CreateTestMessageInFolder(UNSURE, folder_watch) # sleep to ensure filtering. WaitForFilters() # It should no longer be in the Inbox. driver.CheckMessageFilteredFrom(folder_watch) # It should be in the "unsure" folder. spam_msg = driver.FindTestMessage(driver.folder_unsure) if spam_msg is None: TestFailed("The test message vanished from the Inbox, but didn't appear in Unsure") spam_msg.Delete() print "Created an unsure message, and saw it get filtered" def run_tests(manager): "Filtering tests" driver = Driver(manager) manager.Save() # necessary after a full retrain assert driver.manager.config.filter.enabled, "Filtering must be enabled for these tests" assert driver.manager.config.training.train_recovered_spam and \ driver.manager.config.training.train_manual_spam, "Incremental training must be enabled for these tests" driver.CleanAllTestMessages() TestSpamFilter(driver) TestUnsureFilter(driver) TestHamFilter(driver) driver.CleanAllTestMessages() def run_filter_tests(manager): # setup config to save info with the message, and test apply_with_new_config(manager, {"Filter.timer_enabled": False, "Filter.save_spam_info" : True, }, run_tests, manager) apply_with_new_config(manager, {"Filter.timer_enabled": True, "Filter.save_spam_info" : True, }, run_tests, manager) apply_with_new_config(manager, {"Filter.timer_enabled": False, "Filter.save_spam_info" : False, }, run_tests, manager) apply_with_new_config(manager, {"Filter.timer_enabled": True, "Filter.save_spam_info" : False, }, run_tests, manager) def apply_with_new_config(manager, new_config_dict, func, *args): old_config = {} friendly_opts = [] for name, val in new_config_dict.items(): sect_name, opt_name = name.split(".") old_config[sect_name, opt_name] = manager.options.get(sect_name, opt_name) manager.options.set(sect_name, opt_name, val) friendly_opts.append("%s=%s" % (name, val)) manager.addin.FiltersChanged() # to ensure correct filtler in place try: test_name = getattr(func, "__doc__", None) if not test_name: test_name = func.__name__ print "*" * 10, "Running '%s' with %s" % (test_name, ", ".join(friendly_opts)) func(*args) finally: for (sect_name, opt_name), val in old_config.items(): manager.options.set(sect_name, opt_name, val) ############################################################################### # "Non-filter" tests are those that don't require us to create messages and # see them get filtered. def run_nonfilter_tests(manager): # And now some other 'sanity' checks. # Check messages we are unable to score. # Must enable the filtering code for this test msgstore.test_suite_running = False try: print "Scanning all your good mail and spam for some sanity checks..." num_found = num_looked = 0 num_without_headers = num_without_body = num_without_html_body = 0 for folder_ids, include_sub in [ (manager.config.filter.watch_folder_ids, manager.config.filter.watch_include_sub), ([manager.config.filter.spam_folder_id], False), ]: for folder in manager.message_store.GetFolderGenerator(folder_ids, include_sub): for message in folder.GetMessageGenerator(False): # If not ipm.note, then no point reporting - but any # ipm.note messages we don't want to filter should be # reported. num_looked += 1 if num_looked % 500 == 0: print " scanned", num_looked, "messages..." if not message.IsFilterCandidate() and \ message.msgclass.lower().startswith("ipm.note"): if num_found == 0: print "*" * 80 print "WARNING: We found the following messages in your folders that would not be filtered by the addin" print "If any of these messages should be filtered, we have a bug!" num_found += 1 print " %s/%s" % (folder.name, message.subject) headers, body, html_body = message._GetMessageTextParts() if not headers: num_without_headers += 1 if not body: num_without_body += 1 # for HTML, we only check multi-part temp_obj = rfc822.Message(cStringIO.StringIO(headers+"\n\n")) content_type = temp_obj.get("content-type", '') if content_type.lower().startswith("multipart"): if not html_body: num_without_html_body += 1 print "Checked %d items, %d non-filterable items found" % (num_looked, num_found) print "of these items, %d had no headers, %d had no text body and %d had no HTML" % \ (num_without_headers, num_without_body, num_without_html_body) finally: msgstore.test_suite_running = True def run_invalid_id_tests(manager): # Do some tests with invalid message and folder IDs. print "Doing some 'invalid ID' tests - you should see a couple of warning, but no errors or tracebacks" id_no_item = ('0000','0000') # this ID is 'valid' - but there will be no such item id_invalid = ('xxxx','xxxx') # this ID is 'invalid' in that the hex-bin conversion fails id_empty1 = ('','') id_empty2 = () bad_ids = id_no_item, id_invalid, id_empty1, id_empty2 for id in bad_ids: AssertRaises(msgstore.MsgStoreException, manager.message_store.GetMessage, id) # Test 'GetFolderGenerator' works with invalid ids. for id in bad_ids: AssertRaises(msgstore.MsgStoreException, manager.message_store.GetFolder, id) ids = manager.config.filter.watch_folder_ids[:] ids.append(id) found = 0 for f in manager.message_store.GetFolderGenerator(ids, False): found += 1 if found > len(manager.config.filter.watch_folder_ids): raise TestFailed("Seemed to find the extra folder") names = manager.FormatFolderNames(ids, False) if names.find(" Testing MAPI error '%s' in %s" % (mapiutil.GetScodeString(hr), checkpoint) # message moved after we have ID, but before opening. for msf, folder in driver.GetWatchFolderGenerator(): print "Testing in folder '%s'" % msf.GetFQName() if is_ham: msg, words = driver.CreateTestMessageInFolder(HAM, folder) else: msg, words = driver.CreateTestMessageInFolder(SPAM, folder) try: _setup_for_mapi_failure(checkpoint, hr, fail_count) try: # sleep to ensure filtering. WaitForFilters() finally: _restore_mapi_failure() if driver.FindTestMessage(folder) is None: TestFailed("We appear to have filtered a message even though we forced 'not found' failure") finally: if msg is not None: msg.Delete() print "<- Finished MAPI error '%s' in %s" % (mapiutil.GetScodeString(hr), checkpoint) def do_failure_tests(manager): # We setup msgstore to fail for us, then try a few tests. The idea is to # ensure we gracefully degrade in these failures. # We set verbosity to min of 1, as this helps us see how the filters handle # the errors. driver = Driver(manager) driver.CleanAllTestMessages() old_verbose = manager.verbose manager.verbose = max(1, old_verbose) try: _do_single_failure_ham_test(driver, "MAPIMsgStoreMsg._EnsureObject", mapi.MAPI_E_NOT_FOUND) _do_single_failure_ham_test(driver, "MAPIMsgStoreMsg.SetField", -2146644781) _do_single_failure_ham_test(driver, "MAPIMsgStoreMsg.Save", -2146644781) _do_single_failure_ham_test(driver, "MAPIMsgStoreMsg.Save", mapi.MAPI_E_OBJECT_CHANGED, fail_count=1) # SetReadState??? _do_single_failure_spam_test(driver, "MAPIMsgStoreMsg._DoCopyMove", mapi.MAPI_E_TABLE_TOO_BIG) finally: manager.verbose = old_verbose def run_failure_tests(manager): "Forced MAPI failure tests" apply_with_new_config(manager, {"Filter.timer_enabled": True, }, do_failure_tests, manager) apply_with_new_config(manager, {"Filter.timer_enabled": False, }, do_failure_tests, manager) def filter_message_with_event(msg, mgr, all_actions=True): import filter ret = filter._original_filter_message(msg, mgr, all_actions) if ret != "Failed": filter_event.set() # only set if it works return ret def test(manager): from dialogs import SetWaitCursor SetWaitCursor(1) import filter if "_original_filter_message" not in filter.__dict__: filter._original_filter_message = filter.filter_message filter.filter_message = filter_message_with_event try: # restore the plugin config at exit. assert not msgstore.test_suite_running, "already running??" msgstore.test_suite_running = True assert not manager.test_suite_running, "already running??" manager.test_suite_running = True run_filter_tests(manager) run_failure_tests(manager) run_invalid_id_tests(manager) # non-filter tests take alot of time - ask if you want to do them if manager.AskQuestion("Do you want to run the non-filter tests?" \ "\r\n\r\nThese may take some time"): run_nonfilter_tests(manager) print "*" * 20 print "Test suite finished without error!" print "*" * 20 finally: print "Restoring standard configuration..." # Always restore configuration to how we started. msgstore.test_suite_running = False manager.test_suite_running = False manager.LoadConfig() manager.addin.FiltersChanged() # restore original filters. manager.addin.ProcessMissedMessages() SetWaitCursor(0) if __name__=='__main__': print "NOTE: This will NOT work from the command line" print "(it nearly will, and is useful for debugging the tests" print "themselves, so we will run them anyway!)" test() spambayes-1.1a6/Outlook2000/train.py0000664000076500000240000001617611116563014017317 0ustar skipstaff00000000000000#! /usr/bin/env python # Train a classifier from Outlook Mail folders # Authors: Sean D. True, WebReply.Com, Mark Hammond # October, 2002 # Copyright PSF, license under the PSF license import sys import traceback from win32com.mapi import mapi # Note our Message Database uses PR_SEARCH_KEY, *not* PR_ENTRYID, as the # latter changes after a Move operation - see msgstore.py def been_trained_as_ham(msg): if msg.t is None: return False return msg.t == False def been_trained_as_spam(msg): if msg.t is None: return False return msg.t == True def train_message(msg, is_spam, cdata): # Train an individual message. # Returns True if newly added (message will be correctly # untrained if it was in the wrong category), False if already # in the correct category. Catch your own damn exceptions. # If re-classified AND rescore = True, then a new score will # be written to the message (so the user can see some effects) from spambayes.tokenizer import tokenize cdata.message_db.load_msg(msg) was_spam = msg.t if was_spam == is_spam: return False # already correctly classified # Brand new (was_spam is None), or incorrectly classified. stream = msg.GetEmailPackageObject() if was_spam is not None: # The classification has changed; unlearn the old classification. cdata.bayes.unlearn(tokenize(stream), was_spam) # Learn the correct classification. cdata.bayes.learn(tokenize(stream), is_spam) msg.t = is_spam cdata.message_db.store_msg(msg) cdata.dirty = True return True # Untrain a message. # Return: None == not previously trained # True == was_spam # False == was_ham def untrain_message(msg, cdata): from spambayes.tokenizer import tokenize stream = msg.GetEmailPackageObject() cdata.message_db.load_msg(msg) if been_trained_as_spam(msg): assert not been_trained_as_ham(msg), "Can't have been both!" cdata.bayes.unlearn(tokenize(stream), True) cdata.message_db.remove_msg(msg) cdata.dirty = True return True if been_trained_as_ham(msg): assert not been_trained_as_spam(msg), "Can't have been both!" cdata.bayes.unlearn(tokenize(stream), False) cdata.message_db.remove_msg(msg) cdata.dirty = True return False return None def train_folder(f, isspam, cdata, progress): num = num_added = 0 for message in f.GetMessageGenerator(): if progress.stop_requested(): break progress.tick() try: if train_message(message, isspam, cdata): num_added += 1 except: print "Error training message '%s'" % (message,) traceback.print_exc() num += 1 print "Checked", num, "in folder", f.name, "-", num_added, "new entries found." def real_trainer(classifier_data, config, message_store, progress): progress.set_status(_("Counting messages")) num_msgs = 0 for f in message_store.GetFolderGenerator(config.training.ham_folder_ids, config.training.ham_include_sub): num_msgs += f.count for f in message_store.GetFolderGenerator(config.training.spam_folder_ids, config.training.spam_include_sub): num_msgs += f.count progress.set_max_ticks(num_msgs+3) for f in message_store.GetFolderGenerator(config.training.ham_folder_ids, config.training.ham_include_sub): progress.set_status(_("Processing good folder '%s'") % (f.name,)) train_folder(f, 0, classifier_data, progress) if progress.stop_requested(): return for f in message_store.GetFolderGenerator(config.training.spam_folder_ids, config.training.spam_include_sub): progress.set_status(_("Processing spam folder '%s'") % (f.name,)) train_folder(f, 1, classifier_data, progress) if progress.stop_requested(): return progress.tick() if progress.stop_requested(): return # Completed training - save the database # Setup the next "stage" in the progress dialog. progress.set_max_ticks(1) progress.set_status(_("Writing the database...")) classifier_data.Save() # Called back from the dialog to do the actual training. def trainer(mgr, config, progress): rebuild = config.training.rebuild rescore = config.training.rescore if not config.training.ham_folder_ids and not config.training.spam_folder_ids: progress.error(_("You must specify at least one spam or one good folder")) return if rebuild: # Make a new temporary bayes database to use for training. # If we complete, then the manager "adopts" it. # This prevents cancelled training from leaving a "bad" db, and # also prevents mail coming in during training from being classified # with the partial database. import os, manager bayes_base = os.path.join(mgr.data_directory, "$sbtemp$default_bayes_database") mdb_base = os.path.join(mgr.data_directory, "$sbtemp$default_message_database") # determine which db manager to use, and create it. ManagerClass = manager.GetStorageManagerClass() db_manager = ManagerClass(bayes_base, mdb_base) classifier_data = manager.ClassifierData(db_manager, mgr) classifier_data.InitNew() else: classifier_data = mgr.classifier_data # We do this in possibly 3 stages - train, filter, save # re-scoring is much slower than training (as we actually have to save # the message back.) # Saving is really slow sometimes, but we only have 1 tick for that anyway if rescore: stages = (_("Training"), .3), (_("Saving"), .1), (_("Scoring"), .6) else: stages = (_("Training"), .9), (_("Saving"), .1) progress.set_stages(stages) real_trainer(classifier_data, config, mgr.message_store, progress) if progress.stop_requested(): return if rebuild: assert mgr.classifier_data is not classifier_data mgr.AdoptClassifierData(classifier_data) classifier_data = mgr.classifier_data # If we are rebuilding, then we reset the statistics, too. # (But output them to the log for reference). mgr.LogDebug(1, "Session:" + "\r\n".join(\ mgr.stats.GetStats(session_only=True))) mgr.LogDebug(1, "Total:" + "\r\n".join(mgr.stats.GetStats())) mgr.stats.Reset() mgr.stats.ResetTotal(permanently=True) progress.tick() if rescore: # Setup the "filter now" config to what we want. config = mgr.config.filter_now config.only_unread = False config.only_unseen = False config.action_all = False config.folder_ids = mgr.config.training.ham_folder_ids + mgr.config.training.spam_folder_ids config.include_sub = mgr.config.training.ham_include_sub or mgr.config.training.spam_include_sub import filter filter.filterer(mgr, mgr.config, progress) bayes = classifier_data.bayes progress.set_status(_("Completed training with %d spam and %d good messages") % (bayes.nspam, bayes.nham)) def main(): print "Sorry - we don't do anything here any more" if __name__ == "__main__": main() spambayes-1.1a6/PKG-INFO0000664000076500000240000000211211355064627014763 0ustar skipstaff00000000000000Metadata-Version: 1.0 Name: spambayes Version: 1.1a6 Summary: Spam classification system Home-page: http://spambayes.sourceforge.net Author: the spambayes project Author-email: spambayes@python.org License: UNKNOWN Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Environment :: Plugins Classifier: Environment :: Win32 (MS Windows) Classifier: License :: OSI Approved :: Python Software Foundation License Classifier: Operating System :: POSIX Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: Microsoft :: Windows :: Windows 95/98/2000 Classifier: Operating System :: Microsoft :: Windows :: Windows NT/2000 Classifier: Natural Language :: English Classifier: Programming Language :: Python Classifier: Programming Language :: C Classifier: Intended Audience :: End Users/Desktop Classifier: Topic :: Communications :: Email :: Filters Classifier: Topic :: Communications :: Email :: Post-Office :: POP3 Classifier: Topic :: Communications :: Email :: Post-Office :: IMAP spambayes-1.1a6/POP3PROXY.txt0000664000076500000240000001267710646440140016021 0ustar skipstaff00000000000000Additional Information about using the POP3 Proxy. ================================================== Setting Eudora to use different ports under Windows --------------------------------------------------- Eudora can be configured to support multiple pop and SMTP servers on different localhost ports - at least on Windows. You just can't do it from the Tools->Options menu. Eudora reads an ini file, eudora.ini, at startup. The format of this file is documented in the help files. Open Help-Topics, click on index and search on eudora.ini. Under W2K (at least) eudora.ini is either in the eudora install directory or in the user's settings directory depending on how you installed Eudora (probably C:\Documents and Settings\userid\Application Data\Qualcomm\Eudora\eudora.ini) This is how to configure Eudora 5.1 and Spambayes under Windows. Caution: make two copies of eudora.ini - eudora.orig and eudora.new, for example. Edit eudora.new. Close Eudora and copy the edited eudora.new to eudora.ini and then re-start eudora. If you need to go back to your original settings until you get it working with Spambayes, just close Eudora, copy eudora.orig to eudora.ini and restart Eudora. Configure pop3proxy for each of Eudora's personality's pop servers, specifying a separate port for each. I used 1110, 1120, 1130 and 1140 for the four personalities I have in Eudora. Do the same for smtpproxy - again I used 1115, 1125, 1135 and 1145. To configure Eudora: Close Eudora. In eudora.new (or whatever you called it) find the section starting with [Settings]. This contains settings for the dominant personality. Find the line beginning POPAccount. The last part of the account name starting with '@' is the server. Change it to @localhost. Find the lines beginning SMTPServer and POPServer. They will have the server names defined for your dominant personality. Change both server names to localhost Add the following two lines. Use whatever ports you assigned to pop3proxy and smtpproxy for the dominant personality. POPPort=1110 SMTPPort=1115 Setting for other personalities are kept in sections begging with [Persona-personality_name]. For each personality make the same changes as you made for the dominant personality, substituting the proper port numbers. Copy eudora.new to eudora.ini and re-start Eudora. In the password dialog for each personality you should see localhost where you used to see the actual server name. You should see the X-Spambayes headers which you can filter on. In the web interface (localhost:8880) clicking in the Review messages link should show all message processed by Spambayes. For MacOS 9 ----------- As a result of the MacOS multitasking, the proxy may not work very fast (reports suggest that at least a Cube or G4 400 is necessary; YMMV). To handle a network connection to 'localhost', it is easiest to add a host file. If you don't have one already, create a text file called "hosts" in the "Preferences" folder. The content of the file should be: localhost CNAME yourmac.example.com yourmac.example.com A 127.0.0.1 The localhost and 127.0.0.1 values must be exactly like this. If you don't know the right value to use for "yourmac.example.com", put anything that looks like this. The end of the first line must be the same as the start of the second line. When this file is created, go to the "TCP/IP" control panel. Set the user level to 'Administrator'. Click on "Use a host file" and select this file. Save your changes. On the Mac, you can transform a Python script into a double-clickable applet. Just drag & drop the pop3proxy.py script onto the BuildApplet application. You'll get a double-clickable pop3proxy application. To setup: 1. Start pop3proxy and open up a web browser to http://localhost:8880. 2. Click on the Configuration link. 3. Ensure that the servers line looks like: pop3proxy_servers: pop.example.com:110,mail.example.com:110 4. And that the ports line looks like: pop3proxy_ports: 110, 111 To configure Entourage: 1. Go to the 'Tools' menu and choose 'Accounts'. 2. Click on 'New' and choose 'POP'. 3. Fill in the various fields. For the POP server field, put "localhost". 4. For the pop.example.com account, you are done. 5. For the mail.example.com account, in the "Advance receive option" window click on the "Ignore the default POP port" check box and type in 111. To filter with Entourage: The rule can be: If Specific header: X-Spambayes-Classification Contains ham then do nothing If Specific header: X-Spambayes-Classification Contains spam then Move message to folder Spam If Specific header: X-Spambayes-Classification Contains unsure then Move message to folder Unsure To configure Eudora: In Eudora, you will be able to reach only one pop server, since you can configure only one port number for POP. But on this server, you can access more than one account. 1. Go to the 'Tools' menu and choose 'Personalities'. 2. Create a new personality with the POP server as "localhost". Note: You will be able to talk only to the pop.example.com server. To filter with Eudora: The rule can be: Match Header: X-Spambayes-Classification contains ham Action do nothing Match Header: X-Spambayes-Classification contains spam Action Transfer To Spam Match Header: X-Spambayes-Classification contains unsure Action Transfer To Unsure spambayes-1.1a6/pspam/0000775000076500000240000000000011355064626015011 5ustar skipstaff00000000000000spambayes-1.1a6/pspam/pop.py0000664000076500000240000002440311116563017016155 0ustar skipstaff00000000000000"""Spam-filtering proxy for a POP3 server. The implementation uses the SocketServer module to run a multi-threaded POP3 proxy. It adds an X-Spambayes header with a spam probability. It scores a message using a persistent spambayes classifier loaded from a ZEO server. The strategy for adding spam headers is from Richie Hindle's pop3proxy.py. The STAT, LIST, RETR, and TOP commands are intercepted to change the number of bytes the client is told to expect and/or to insert the spam header. The proxy can connect to any real POP3 server. It parses the USER command to figure out the address of the real server. It expects the USER argument to follow this format user@server[:port]. For example, if you configure your POP client to send USER jeremy@example.com:111. It will connect to a server on port 111 at example.com and send it the command USER jeremy. XXX A POP3 server sometimes adds the number of bytes in the +OK response to some commands when the POP3 spec doesn't require it to. In those case, the proxy does not re-write the number of bytes. I assume the clients won't be confused by this behavior, because they shouldn't be expecting to see the number of bytes. POP3 is documented in RFC 1939. """ import SocketServer try: import cStringIO as StringIO except ImportError: import StringIO import email import re import socket import sys import time import zLOG from spambayes.tokenizer import tokenize import pspam.database from spambayes.Options import options HEADER = "X-Spambayes: %5.3f\r\n" HEADER_SIZE = len(HEADER % 0.0) VERSION = 0.1 class POP3ProxyServer(SocketServer.ThreadingTCPServer): allow_reuse_address = True def __init__(self, addr, handler, classifier, log, zodb): SocketServer.ThreadingTCPServer.__init__(self, addr, handler) self.classifier = classifier self.log = log self.zodb = zodb class LogWrapper: def __init__(self, log, file): self.log = log self.file = file def readline(self): line = self.file.readline() self.log.write(line) return line def write(self, buf): self.log.write(buf) return self.file.write(buf) def close(self): self.file.close() class POP3RequestHandler(SocketServer.StreamRequestHandler): """Act as proxy between POP client and server.""" def read_user(self): # XXX This could be cleaned up a bit. line = self.rfile.readline() if line == "": return False parts = line.split() if parts[0] != "USER": self.wfile.write("-ERR Invalid command; must specify USER first\n") return False user = parts[1] i = user.rfind("@") username = user[:i] server = user[i+1:] i = server.find(":") if i == -1: server = server, 110 else: port = int(server[i+1:]) server = server[:i], port zLOG.LOG("POP3", zLOG.INFO, "Got connect for %s" % repr(server)) self.connect_pop(server) self.pop_wfile.write("USER %s\r\n" % username) resp = self.pop_rfile.readline() # As long the server responds OK, just swallow this reponse. if resp.startswith("+OK"): return True else: return False def connect_pop(self, pop_server): # connect to the pop server s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(pop_server) self.pop_rfile = LogWrapper(self.server.log, s.makefile("rb")) # the write side should be unbuffered self.pop_wfile = LogWrapper(self.server.log, s.makefile("wb", 0)) def close_pop(self): self.pop_rfile.close() self.pop_wfile.close() def handle(self): zLOG.LOG("POP3", zLOG.INFO, "Connection from %s" % repr(self.client_address)) self.server.zodb.sync() self.sess_retr_count = 0 self.wfile.write("+OK pspam/pop %s\r\n" % VERSION) # First read the USER command to get the real server's name if not self.read_user(): zLOG.LOG("POP3", zLOG.INFO, "Did not get valid USER") return try: self.handle_pop() finally: self.close_pop() if self.sess_retr_count == 1: ending = "" else: ending = "s" zLOG.LOG("POP3", zLOG.INFO, "Ending session (%d message%s retrieved)" % (self.sess_retr_count, ending)) _multiline = {"RETR": True, "TOP": True,} _multiline_noargs = {"LIST": True, "UIDL": True,} def is_multiline(self, command, args): if command in self._multiline: return True if command in self._multiline_noargs and not args: return True return False def parse_request(self, req): parts = req.split() req = parts[0] args = tuple(parts[1:]) return req, args def handle_pop(self): # send the initial server hello hello = self.pop_rfile.readline() self.wfile.write(hello) # now get client requests and return server responses while 1: line = self.rfile.readline() if line == '': break self.pop_wfile.write(line) if not self.handle_pop_response(line): break def handle_pop_response(self, req): # Return True if connection is still open cmd, args = self.parse_request(req) multiline = self.is_multiline(cmd, args) firstline = self.pop_rfile.readline() zLOG.LOG("POP3", zLOG.DEBUG, "command %s multiline %s resp %s" % (cmd, multiline, firstline.strip())) if multiline: # Collect the entire response as one string resp = StringIO.StringIO() while 1: line = self.pop_rfile.readline() resp.write(line) # The response is finished if we get . or an error. # XXX should handle byte-stuffed response if line == ".\r\n": break if line.startswith("-ERR"): break buf = resp.getvalue() else: buf = None handler = getattr(self, "handle_%s" % cmd, None) if handler: firstline, buf = handler(cmd, args, firstline, buf) self.wfile.write(firstline) if buf is not None: self.wfile.write(buf) if cmd == "QUIT": return False else: return True def handle_RETR(self, cmd, args, firstline, resp): if not resp: return firstline, resp try: msg = email.message_from_string(resp) except email.Errors.MessageParseError, err: zLOG.LOG("POP3", zLOG.WARNING, "Failed to parse msg: %s" % err, error=sys.exc_info()) resp = self.message_parse_error(resp) else: self.score_msg(msg) resp = msg.as_string() self.sess_retr_count += 1 return firstline, resp def handle_TOP(self, cmd, args, firstline, resp): # XXX Just handle TOP like RETR? return self.handle_RETR(cmd, args, firstline, resp) rx_STAT = re.compile("\+OK (\d+) (\d+)(.*)", re.DOTALL) def handle_STAT(self, cmd, args, firstline, resp): # STAT returns the number of messages and the total size. The # proxy must add the size of new headers to the total size. # Example: +OK 3 340 mo = self.rx_STAT.match(firstline) if mo is None: return firstline, resp count, size, extra = mo.group(1, 2, 3) count = int(count) size = int(size) size += count * HEADER_SIZE firstline = "+OK %d %d%s" % (count, size, extra) return firstline, resp rx_LIST = re.compile("\+OK (\d+) (\d+)(.*)", re.DOTALL) rx_LIST_2 = re.compile("(\d+) (\d+)(.*)", re.DOTALL) def handle_LIST(self, cmd, args, firstline, resp): # If there are no args, LIST returns size info for each message. # If there is an arg, LIST return number and size for one message. mo = self.rx_LIST.match(firstline) if mo: # a single-line response n, size, extra = mo.group(1, 2, 3) size = int(size) + HEADER_SIZE firstline = "+OK %s %d%s" % (n, size, extra) return firstline, resp else: # possibility a multiline response if not firstline.startswith("+OK"): return firstline, resp # update each line of the response L = [] for line in resp.split("\r\n"): if not line: continue mo = self.rx_LIST_2.match(line) if not mo: L.append(line) else: n, size, extra = mo.group(1, 2, 3) size = int(size) + HEADER_SIZE L.append("%s %d%s" % (n, size, extra)) return firstline, "\r\n".join(L) def message_parse_error(self, buf): # We get an error parsing the message. We've already told the # client to expect more bytes that this buffer contains, but # there's not clean way to add the header. self.server.log.write("# error: %s\n" % repr(buf)) # XXX what to do? list's just add it after the first line score = self.server.classifier.spamprob(tokenize(buf)) L = buf.split("\n") L.insert(1, HEADER % score) return "\n".join(L) def score_msg(self, msg): score = self.server.classifier.spamprob(tokenize(msg)) msg.add_header("X-Spambayes", "%5.3f" % score) def main(): db = pspam.database.open() conn = db.open() r = conn.root() profile = r["profile"] log = open("pop.log", "ab") print >> log, "+PROXY start", time.ctime() server = POP3ProxyServer(('', int(options["pop3proxy", "listen_ports"][0])), POP3RequestHandler, profile.classifier, log, conn, ) server.serve_forever() if __name__ == "__main__": main() spambayes-1.1a6/pspam/pspam/0000775000076500000240000000000011355064626016131 5ustar skipstaff00000000000000spambayes-1.1a6/pspam/pspam/__init__.py0000664000076500000240000000101210646440121020222 0ustar skipstaff00000000000000"""Package for interacting with VM folders. Design notes go here. Use ZODB to store training data and classifier. The spam and ham data are culled from sets of folders. The actual tokenized messages are stored in a training database. When the folder changes, the training data is updated. - Updates are incremental. - Changes to a folder are detected based on mtime and folder size. - The contents of the folder are keyed on message-id. - If a message is removed from a folder, it is removed from training data. """ spambayes-1.1a6/pspam/pspam/database.py0000664000076500000240000000120310646440121020231 0ustar skipstaff00000000000000from spambayes.Options import options import ZODB from ZEO.ClientStorage import ClientStorage import zLOG import os def logging(): os.environ["STUPID_LOG_FILE"] = options["ZODB", "event_log_file"] os.environ["STUPID_LOG_SEVERITY"] = str(options["ZODB", "event_log_severity"]) zLOG.initialize() def open(): addr = options["ZODB", "zeo_addr"] if addr and addr[0] == "(" and addr[-1] == ")": s, p = tuple(addr[1:-1].split(',', 1)) addr = s, int(p) cs = ClientStorage(addr) db = ZODB.DB(cs, cache_size=options["ZODB", "cache_size"]) return db spambayes-1.1a6/pspam/pspam/folder.py0000664000076500000240000000366511116563023017756 0ustar skipstaff00000000000000import ZODB from Persistence import Persistent from BTrees.OOBTree import OOBTree, OOSet, difference import email import mailbox import os import stat from pspam.message import PMessage def factory(fp): try: return email.message_from_file(fp, PMessage) except email.Errors.MessageError, msg: print msg return PMessage() class Folder(Persistent): def __init__(self, path): self.path = path self.mtime = 0 self.size = 0 self.messages = OOBTree() def _stat(self): t = os.stat(self.path) self.mtime = t[stat.ST_MTIME] self.size = t[stat.ST_SIZE] def changed(self): t = os.stat(self.path) if (t[stat.ST_MTIME] != self.mtime or t[stat.ST_SIZE] != self.size): return True else: return False def read(self): """Return messages added and removed from folder. Two sets of message objects are returned. The first set is messages that were added to the folder since the last read. The second set is the messages that were removed from the folder since the last read. The code assumes messages are added and removed but not edited. """ mbox = mailbox.UnixMailbox(open(self.path, "rb"), factory) self._stat() cur = OOSet() new = OOSet() while 1: msg = mbox.next() if msg is None: break msgid = msg["message-id"] cur.insert(msgid) if not self.messages.has_key(msgid): self.messages[msgid] = msg new.insert(msg) removed = difference(self.messages, cur) for msgid in removed.keys(): del self.messages[msgid] # XXX perhaps just return the OOBTree for removed? return new, OOSet(removed.values()) if __name__ == "__main__": f = Folder("/home/jeremy/Mail/INBOX") spambayes-1.1a6/pspam/pspam/message.py0000664000076500000240000000025010646440121020112 0ustar skipstaff00000000000000import ZODB from Persistence import Persistent from email.Message import Message class PMessage(Message, Persistent): def __hash__(self): return id(self) spambayes-1.1a6/pspam/pspam/profile.py0000664000076500000240000000622511116563026020141 0ustar skipstaff00000000000000"""Spam/ham profile for a single VM user.""" import ZODB from ZODB.PersistentList import PersistentList from Persistence import Persistent from BTrees.OOBTree import OOBTree from spambayes import classifier from spambayes.tokenizer import tokenize from pspam.folder import Folder from spambayes.Options import options import os def open_folders(dir, names, klass): L = [] for name in names: path = os.path.join(dir, name) L.append(klass(path)) return L import time _start = None def log(s): global _start if _start is None: _start = time.time() print round(time.time() - _start, 2), s class IterOOBTree(OOBTree): def iteritems(self): return self.items() class WordInfo(Persistent): def __init__(self): self.spamcount = self.hamcount = 0 def __repr__(self): return "WordInfo(%r, %r)" % (self.spamcount, self.hamcount) ##class PMetaInfo(classifier.MetaInfo, Persistent): ## pass class PMetaInfo(Persistent): pass class PBayes(classifier.Bayes, Persistent): WordInfoClass = WordInfo def __init__(self): classifier.Bayes.__init__(self) self.wordinfo = IterOOBTree() self.meta = PMetaInfo() # XXX what about the getstate and setstate defined in base class class Profile(Persistent): FolderClass = Folder def __init__(self, folder_dir): self._dir = folder_dir self.classifier = PBayes() self.hams = PersistentList() self.spams = PersistentList() def add_ham(self, folder): p = os.path.join(self._dir, folder) f = self.FolderClass(p) self.hams.append(f) def add_spam(self, folder): p = os.path.join(self._dir, folder) f = self.FolderClass(p) self.spams.append(f) def update(self): """Update classifier from current folder contents.""" changed1 = self._update(self.hams, False) changed2 = self._update(self.spams, True) ## if changed1 or changed2: ## self.classifier.update_probabilities() get_transaction().commit() log("updated probabilities") def _update(self, folders, is_spam): changed = False for f in folders: log("update from %s" % f.path) added, removed = f.read() if added: log("added %d" % len(added)) if removed: log("removed %d" % len(removed)) get_transaction().commit() if not (added or removed): continue changed = True # It's important not to commit a transaction until # after update_probabilities is called in update(). # Otherwise some new entries will cause scoring to fail. for msg in added.keys(): self.classifier.learn(tokenize(msg), is_spam) del added get_transaction().commit(1) log("learned") for msg in removed.keys(): self.classifier.unlearn(tokenize(msg), is_spam) if removed: log("unlearned") del removed get_transaction().commit(1) return changed spambayes-1.1a6/pspam/README.txt0000664000076500000240000000155610646440121016504 0ustar skipstaff00000000000000pspam: persistent spambayes filtering system -------------------------------------------- pspam uses a POP proxy to score incoming messages, a set of VM folders to manage training data, and a ZODB database to manage data used by the various applications. The current code only works with a patched version of classifier.py. Remove the object base class & change the class used to create new WordInfo objects. This directory contains: pspam -- a Python package pop.py -- a POP proxy based on SocketServer scoremsg.py -- prints the evidence for a single message read from stdin update.py -- a script to update training data from folders vmspam.ini -- a sample configuration file zeo.sh -- a script to start a ZEO server zeo.bat -- a script to start a ZEO server on Windows The code depends on ZODB3, which you can download from http://www.zope.org/Products/StandaloneZODB. spambayes-1.1a6/pspam/scoremsg.py0000775000076500000240000000144511116563031017201 0ustar skipstaff00000000000000#! /usr/bin/env python """Score a message provided on stdin and show the evidence.""" import sys import email import locale from types import UnicodeType import pspam.database from spambayes.tokenizer import tokenize def main(fp): charset = locale.getdefaultlocale()[1] if not charset: charset = 'us-ascii' db = pspam.database.open() r = db.open().root() p = r["profile"] msg = email.message_from_file(fp) prob, evidence = p.classifier.spamprob(tokenize(msg), True) print "Score:", prob print print "Clues" print "-----" for clue, prob in evidence: if isinstance(clue, UnicodeType): clue = clue.encode(charset, 'replace') print clue, prob ## print ## print msg if __name__ == "__main__": main(sys.stdin) spambayes-1.1a6/pspam/update.py0000664000076500000240000000257011116563035016642 0ustar skipstaff00000000000000import getopt import os import sys import pspam.database from pspam.profile import Profile from spambayes.Options import options def folder_exists(L, p): """Return true folder with path p exists in list L.""" for f in L: if f.path == p: return True return False def main(rebuild=False): db = pspam.database.open() r = db.open().root() profile = r.get("profile") if profile is None or rebuild: # if there is no profile, create it profile = r["profile"] = Profile(options["ZODB", "folder_dir"]) get_transaction().commit() # check for new folders of training data for ham in options["ZODB", "ham_folders"].split(os.pathsep): p = os.path.join(options["ZODB", "folder_dir"], ham) if not folder_exists(profile.hams, p): profile.add_ham(p) for spam in options["ZODB", "spam_folders"].split(os.pathsep): p = os.path.join(options["ZODB", "folder_dir"], spam) if not folder_exists(profile.spams, p): profile.add_spam(p) get_transaction().commit() # read new messages from folders profile.update() get_transaction().commit() db.close() if __name__ == "__main__": FORCE_REBUILD = False opts, args = getopt.getopt(sys.argv[1:], 'F') for k, v in opts: if k == '-F': FORCE_REBUILD = True main(FORCE_REBUILD) spambayes-1.1a6/pspam/vmspam.ini0000664000076500000240000000022510646440121017002 0ustar skipstaff00000000000000[ZODB] folder_dir: /home/jeremy/Mail spam_folders: train/spam ham_folders: train/ham zeo_addr: /var/tmp/zeospam event_log_file: /var/tmp/zeospam.log spambayes-1.1a6/pspam/zeo.sh0000775000076500000240000000026610646440121016137 0ustar skipstaff00000000000000#! /bin/bash export STUPID_LOG_FILE=/var/tmp/zeospam.log export LIBDIR=/usr/local/lib/python2.3/site-packages python2.3 $LIBDIR/ZEO/start.py -U /var/tmp/zeospam /var/tmp/zeospam.fs spambayes-1.1a6/README-DEVEL.txt0000664000076500000240000007623011150447244016226 0ustar skipstaff00000000000000Copyright (C) 2002-2009 Python Software Foundation; All Rights Reserved The Python Software Foundation (PSF) holds copyright on all material in this project. You may use it under the terms of the PSF license; see LICENSE.txt. Assorted clues. What's Here? ============ Lots of mondo cool partially documented code. What else could there be ? The focus of this project so far has not been to produce the fastest or smallest filters, but to set up a flexible pure-Python implementation for doing algorithm research. Lots of people are making fast/small implementations, and it takes an entirely different kind of effort to make genuine algorithm improvements. I think we've done quite well at that so far. The focus of this codebase may change to small/fast later -- as is, the false positive rate has gotten too small to measure reliably across test sets with 4000 hams + 2750 spams, and the f-n rate has also gotten too small to measure reliably across that much training data. The code in this project requires Python 2.2 (or later). You should definitely check out the FAQ: http://spambayes.org/faq.html Getting Source Code =================== The SpamBayes project source code is hosted at SourceForge (http://spambayes.sourceforge.net/). Access is via Subversion. Running Unit Tests ================== SpamBayes has a currently incomplete set of unit tests, not all of which pass, due, in part, to bit rot. We are working on getting the unit tests to run using the `nose `_ package. After downloading and installing nose, you can run the current unit tests on Unix-like systems like so from the SpamBayes top-level directory:: TMPDIR=/tmp BAYESCUSTOMIZE= nosetests -v . 2>&1 \ | sed -e "s:$(pwd)/::" \ -e "s:$(python -c 'import sys ; print sys.exec_prefix')/::" \ | tee failing-unit-tests.txt The file, failing-unit-tests.txt, is checked into the Subversion repository at the top level using Python from Subversion (currently 2.7a0). You can look at it for any failing unit tests and work to get them passing, or write new tests. Primary Core Files ================== Options.py Uses ConfigParser to allow fiddling various aspects of the classifier, tokenizer, and test drivers. Create a file named bayescustomize.ini to alter the defaults. Modules wishing to control aspects of their operation merely do from Options import options near the start, and consult attributes of options. To see what options are available, import Options.py and do print Options.options.display_full() This will print out a detailed description of each option, the allowed values, and so on. (You can pass in a section or section and option name to display_full if you don't want the whole list). As an alternative to bayescustomize.ini, you can set the environment variable BAYESCUSTOMIZE to a list of one or more .ini files, these will be read in, in order, and applied to the options. This allows you to tweak individual runs by combining fragments of .ini files. The character used to separate different .ini files is platform-dependent. On Unix, Linux and Mac OS X systems it is ':'. On Windows it is ';'. On Mac OS 9 and earlier systems it is a NL character. classifier.py The classifier, which is the soul of the method. tokenizer.py An implementation of tokenize() that Tim can't seem to help but keep working on . Generates a token stream from a message, which the classifier trains on or predicts against. chi2.py A collection of statistics functions. Apps ==== sb_filter.py A simpler hammie front-end that doesn't print anything. Useful for procmail filtering and scoring from your MUA. sb_mboxtrain.py Trainer for Maildir, MH, or mbox mailboxes. Remembers which messages it saw the last time you ran it, and will only train on new messages or messages which should be retrained. The idea is to run this automatically every night on your Inbox and Spam folders, and then sort misclassified messages by hand. This will work with any IMAP4 mail client, or any client running on the server. sb_server.py A spam-classifying POP3 proxy. It adds a spam-judgment header to each mail as it's retrieved, so you can use your email client's filters to deal with them without needing to fiddle with your email delivery system. Also acts as a web server providing a user interface that allows you to train the classifier, classify messages interactively, and query the token database. This piece may at some point be split out into a separate module. If the appropriate options are set, also serves a message training SMTP proxy. It sits between your email client and your SMTP server and intercepts mail to set ham and spam addresses. All other mail is simply passed through to the SMTP server. sb_mailsort.py A delivery agent that uses a CDB of word probabilities and delivers a message to one of two Maildir message folders, depending on the classifier score. Note that both Maildirs must be on the same device. sb_xmlrpcserver.py A stab at making hammie into a client/server model, using XML-RPC. sb_client.py A client for sb_xmlrpcserver.py. sb_imapfilter.py A spam-classifying and training application for use with IMAP servers. You can specify folders that contain mail to train as ham/spam, and folders that contain mail to classify, and the filter will do so. Test Driver Core ================ Tester.py A test-driver class that feeds streams of msgs to a classifier instance, and keeps track of right/wrong percentages and lists of false positives and false negatives. TestDriver.py A flexible higher layer of test helpers, building on Tester above. For example, it's usable for building simple test drivers, NxN test grids, and N-fold cross-validation drivers. See also rates.py, cmp.py, and table.py below. msgs.py Some simple classes to wrap raw msgs, and to produce streams of msgs. The test drivers use these. Concrete Test Drivers ===================== mboxtest.py A concrete test driver like timtest.py, but working with a pair of mailbox files rather than the specialized timtest setup. timcv.py An N-fold cross-validating test driver. Assumes "a standard" data directory setup (see below)) rather than the specialized mboxtest setup. N classifiers are built. 1 run is done with each classifier. Each classifier is trained on N-1 sets, and predicts against the sole remaining set (the set not used to train the classifier). mboxtest does the same. This (or mboxtest) is the preferred way to test when possible: it makes best use of limited data, and interpreting results is straightforward. timtest.py A concrete test driver like mboxtest.py, but working with "a standard" test data setup (see below). This runs an NxN test grid, skipping the diagonal. N classifiers are built. N-1 runs are done with each classifier. Each classifier is trained on 1 set, and predicts against each of the N-1 remaining sets (those not used to train the classifier). This is a much harder test than timcv, because it trains on N-1 times less data, and makes each classifier predict against N-1 times more data than it's been taught about. It's harder to interpret the results of timtest (than timcv) correctly, because each msg is predicted against N-1 times overall. So, e.g., one terribly difficult spam or ham can count against you N-1 times. Test Utilities ============== rates.py Scans the output (so far) produced by TestDriver.Drive(), and captures summary statistics. cmp.py Given two summary files produced by rates.py, displays an account of all the f-p and f-n rates side-by-side, along with who won which (etc), the change in total # of unique false positives and negatives, and the change in average f-p and f-n rates. table.py Summarizes the high-order bits from any number of summary files, in a compact table. fpfn.py Given one or more TestDriver output files, prints list of false positive and false negative filenames, one per line. Test Data Utilities =================== cleanarch.py A script to repair mbox archives by finding "Unix From" lines that should have been escaped, and escaping them. unheader.py A script to remove unwanted headers from an mbox file. This is mostly useful to delete headers which incorrectly might bias the results. In default mode, this is similar to 'spamassassin -d', but much, much faster. loosecksum.py A script to calculate a "loose" checksum for a message. See the text of the script for an operational definition of "loose". rebal.py Evens out the number of messages in "standard" test data folders (see below). Needs generalization (e.g., Ham and 4000 are hardcoded now). mboxcount.py Count the number of messages (both parseable and unparseable) in mbox archives. split.py splitn.py Split an mbox into random pieces in various ways. Tim recommends using "the standard" test data set up instead (see below). splitndirs.py Like splitn.py (above), but splits an mbox into one message per file in "the standard" directory structure (see below). This does an approximate split; rebal.py (above) can be used afterwards to even out the number of messages per folder. runtest.sh A Bourne shell script (for Unix) which will run some test or other. I (Neale) will try to keep this updated to test whatever Tim is currently asking for. The idea is, if you have a standard directory structure (below), you can run this thing, go have some tea while it works, then paste the output to the SpamBayes list for good karma. Standard Test Data Setup ======================== Barry gave Tim mboxes, but the spam corpus he got off the web had one spam per file, and it only took two days of extreme pain to realize that one msg per file is enormously easier to work with when testing: you want to split these at random into random collections, you may need to replace some at random when testing reveals spam mistakenly called ham (and vice versa), etc -- even pasting examples into email is much easier when it's one msg per file (and the test drivers make it easy to print a msg's file path). The directory structure under my spambayes directory looks like so: Data/ Spam/ Set1/ (contains 1375 spam .txt files) Set2/ "" Set3/ "" Set4/ "" Set5/ "" Set6/ "" Set7/ "" Set9/ "" Set9/ "" Set10/ "" reservoir/ (contains "backup spam") Ham/ Set1/ (contains 2000 ham .txt files) Set2/ "" Set3/ "" Set4/ "" Set5/ "" Set6/ "" Set7/ "" Set8/ "" Set9/ "" Set10/ "" reservoir/ (contains "backup ham") Every file at the deepest level is used (not just files with .txt extensions). The files don't need to have a "Unix From" header before the RFC-822 message (i.e. a line of the form "From
    "). If you use the same names and structure, huge mounds of the tedious testing code will work as-is. The more Set directories the merrier, although you want at least a few hundred messages in each one. The "reservoir" directories contain a few thousand other random hams and spams. When a ham is found that's really spam, move it into a spam directory, then use the rebal.py utility to rebalance the Set directories moving random message(s) into and/or out of the reservoir directories. The reverse works as well (finding ham in your spam directories). The hams are 20,000 msgs selected at random from a python-list archive. The spams are essentially all of Bruce Guenter's 2002 spam archive: The sets are grouped into pairs in the obvious way: Spam/Set1 with Ham/Set1, and so on. For each such pair, timtest trains a classifier on that pair, then runs predictions on each of the other pairs. In effect, it's a NxN test grid, skipping the diagonal. There's no particular reason to avoid predicting against the same set trained on, except that it takes more time and seems the least interesting thing to try. Later, support for N-fold cross validation testing was added, which allows more accurate measurement of error rates with smaller amounts of training data. That's recommended now. timcv.py is to cross-validation testing as the older timtest.py is to grid testing. timcv.py has grown additional arguments to allow using only a random subset of messages in each Set. CAUTION: The partitioning of your corpora across directories should be random. If it isn't, bias creeps in to the test results. This is usually screamingly obvious under the NxN grid method (rates vary by a factor of 10 or more across training sets, and even within runs against a single training set), but harder to spot using N-fold c-v. Testing a change and posting the results ======================================== (Adapted from clues Tim posted on the spambayes and spambayes-dev lists) Firstly, setup your data as above; it's really not worth the hassle to come up with a different scheme. If you use the Outlook plug-in, the export.py script in the Outlook2000 directory will export all the spam and ham in your 'training' folders for you into this format (or close enough). Basically the idea is that you should have 10 sets of data, each with 200 to 500 messages in them. Obviously if you're testing something to do with the size of a corpus, you'll want to change that. You then want to run timcv.py -n 10 > std.txt (call std.txt whatever you like), and then rates.py std.txt You end up with two files, std.txt, which has the raw results, and stds.txt, which has more of a summary of the results. Now make the change to the code or options, and repeat the process, giving the files different names (note that rates.py will automatically choose the name for the output file, based on the input one). You've now got the data you need, but you have to interpret it. The simplest way of all is just to post it to spambayes-dev@python.org and let someone else do it for you . The data you should post is the output of cmp.py stds.txt alts.txt along with the output of table.py stds.txt alts.txt (note that these just print to stdout). Other information you can find in the 'raw' output (std.txt, above) are histograms of the ham/spam spread, and a copy of the options settings. Interpreting cmp.py output -------------------------- (Using an example from Tim on spambayes-dev) > cv_octs.txt -> cv_oct_subjs.txt > -> tested 488 hams & 897 spams against 1824 hams & 3501 spams > -> tested 462 hams & 863 spams against 1850 hams & 3535 spams > -> tested 475 hams & 863 spams against 1837 hams & 3535 spams > -> tested 430 hams & 887 spams against 1882 hams & 3511 spams > -> tested 457 hams & 888 spams against 1855 hams & 3510 spams > -> tested 488 hams & 897 spams against 1824 hams & 3501 spams > -> tested 462 hams & 863 spams against 1850 hams & 3535 spams > -> tested 475 hams & 863 spams against 1837 hams & 3535 spams > -> tested 430 hams & 887 spams against 1882 hams & 3511 spams > -> tested 457 hams & 888 spams against 1855 hams & 3510 spams > > false positive percentages > 0.000 0.000 tied > 0.000 0.000 tied > 0.000 0.000 tied > 0.000 0.000 tied > 0.219 0.219 tied > > won 0 times > tied 5 times > lost 0 times So all 5 runs tied on FP. That tells us much more than that the *net* effect across 5 runs was nil on FP: it tells us that there are no hidden glitches hiding behind that "net nothing" -- it was no change across the board. > total unique fp went from 1 to 1 tied > mean fp % went from 0.0437636761488 to 0.0437636761488 tied > > false negative percentages > 2.007 2.007 tied > 1.390 1.390 tied > 1.622 1.622 tied > 2.029 1.917 won -5.52% > 2.703 2.477 won -8.36% > > won 2 times > tied 3 times > lost 0 times When evaluating a small change, I'm heartened to see that in no run did it lose. At worst it tied, and twice it helped a little. That's encouraging. What the histograms would tell us that we can't tell from this is whether you could have done just as well without the change by raising your ham cutoff a little. That would also tie on FP, and *may* also get rid of the same number (or even more) of FN. > total unique fn went from 86 to 83 won -3.49% > mean fn % went from 1.95029003772 to 1.88269707836 won -3.47% > > ham mean ham sdev > 0.57 0.58 +1.75% 4.63 4.77 +3.02% > 0.08 0.07 -12.50% 1.20 1.01 -15.83% > 0.36 0.29 -19.44% 3.61 3.23 -10.53% > 0.08 0.11 +37.50% 0.89 1.18 +32.58% > 0.72 0.76 +5.56% 6.80 7.06 +3.82% > > ham mean and sdev for all runs > 0.37 0.37 +0.00% 4.10 4.16 +1.46% That's a good example of grand averages hiding the truth: the averaged change in the mean ham score was 0 across all 5 runs, but *within* the 5 runs it slobbered around wildly, from decreasing 20% to increasing 40%(!). > spam mean spam sdev > 96.43 96.44 +0.01% 15.89 15.89 +0.00% > 97.01 97.07 +0.06% 13.79 13.70 -0.65% > 97.14 97.16 +0.02% 14.05 14.02 -0.21% > 96.52 96.56 +0.04% 15.65 15.52 -0.83% > 95.53 95.63 +0.10% 17.47 17.31 -0.92% > > spam mean and sdev for all runs > 96.52 96.57 +0.05% 15.46 15.37 -0.58% That's good to see: it's a consistent win for spam scores across runs, although an almost imperceptible one. It's good when the mean spam score rises, and it's good when sdev (for ham or spam) decreases. > ham/spam mean difference: 96.15 96.20 +0.05 This is a slight win for the chance, although seeing the details gives cause to worry some about the effect on ham: the ham sdev increased overall, and the effects on ham mean and ham sdev varied wildly across runs. OTOH, the "before" numbers for ham mean and ham sdev varied wildly across runs already. That gives cause to worry some about the data . Making a source release ======================= Source releases are built with distutils. Here's how I (Richie) have been building them. I do this on a Windows box, partly so that the zip release can have Windows line endings without needing to run a conversion script. I don't think that's actually necessary, because everything would work on Windows even with Unix line endings, but you couldn't load the files into Notepad and sometimes it's convenient to do so. End users might not even have any other text editor, so it make things like the README unREADable. 8-) Anthony would rather eat live worms than trying to get a sane environment on Windows, so his approach to building the zip file is at the end. o If any new file types have been added since last time (eg. 1.0a5 went out without the Windows .rc and .h files) then add them to MANIFEST.in. If there are any new scripts or packages, add them to setup.py. Test these changes (by building source packages according to the instructions below) then commit your edits. o Checkout the 'spambayes' module twice, once with Windows line endings and once with Unix line endings (I use WinCVS for this, using "Admin / Preferences / Globals / Checkout text files with the Unix LF". If you use TortoiseCVS, like Tony, then the option is on the Options tab in the checkout dialog). o Change spambayes/__init__.py to contain the new version number but don't commit it yet, just in case something goes wrong. o Note that if you cheated above, and used an existing checkout, you need to ensure that you don't have extra files in there. For example, if you have a few thousand email messages in testtools/Data, setup.py will take a *very* long time. o In the Windows checkout, run "python setup.py sdist --formats zip" o In the Unix checkout, run "python setup.py sdist --formats gztar" o Take the resulting spambayes-1.0a5.zip and spambayes-1.0a5.tar.gz, and test the former on Windows (ideally in a freshly-installed Python environment; I keep a VMWare snapshot of a clean Windows installation for this, but that's probably overkill 8-) and test the latter on Unix (a Debian VMWare box in my case). o If you can, rename these with "rc" at the end, and make them available to the spambayes-dev crowd as release candidates. If all is OK, then fix the names (or redo this) and keep going. o Dance the SourceForge release dance: http://sourceforge.net/docman/display_doc.php?docid=6445&group_id=1#filereleasesteps When it comes to the "what's new" and the ChangeLog, I cut'n'paste the relevant pieces of WHAT_IS_NEW.txt and CHANGELOG.txt into the form, and check the "Keep my preformatted text" checkbox. o Now commit spambayes/__init__.py and tag the whole checkout - see the existing tag names for the tag name format. o In either checkout, run "python setup.py register" to register the new version with PyPI. o Update download.ht with checksums, links, and sizes for the files. From release 1.1 doing a "setup.py sdist" will generate checksums and sizes for you, and print out the results to stdout. o Create OpenPGP/PGP signatures for the files. Using GnuPG: % gpg -sab spambayes-1.0.1.zip % gpg -sab spambayes-1.0.1.tar.gz % gpg -sab spambayes-1.0.1.exe Put the created *.asc files in the "sigs" directory of the website. (Note that when you update the website, you will need to manually ssh to shell1.sourceforge.net and chmod these files so that people can access them.) o If your public key isn't already linked to on the Download page, put it there. o Update the website News, Download and Windows sections. o Update reply.txt in the website repository as needed (it specifies the latest version). Then let Tim, Barry, Tony, or Skip know that they need to update the autoresponder. o Run "make install version" in the website directory to push the new version file, so that "Check for new version" works. o Add '+' to the end of spambayes/__init__.py's __version__, to differentiate CVS users, and check this change in. After a number of changes have been checked in, this can be incremented and have "a0" added to the end. For example, with a 1.1 release: [before the release process] '1.1rc1' [during the release process] '1.1' [after the release process] '1.1+' [later] '1.2a0' Then announce the release on the mailing lists and watch the bug reports roll in. 8-) Anthony's Alternate Approach to Building the Zipfile o Unpack the tarball somewhere, making a spambayes-1.0a7 directory (version number will obviously change in future releases) o Run the following two commands: find spambayes-1.0a7 -type f -name '*.txt' | xargs zip -l sb107.zip find spambayes-1.0a7 -type f \! -name '*.txt' | xargs zip sb107.zip o This makes a tarball where the .txt files are mangled, but everything else is left alone. Making a binary release ======================= The binary release includes both sb_server and the Outlook plug-in and is an installer for Windows (98 and above) systems. In order to have COM typelibs that work with Outlook 2000, 2002 and 2003, you need to build the installer on a system that has Outlook 2000 (not a more recent version). You also need to have InnoSetup, pywin32, resourcepackage and py2exe installed. o Get hold of a fresh copy of the source (Windows line endings, presumably). o Run the setup.py file in the spambayes/Outlook2000/docs directory to generate the dynamic documentation. o Run sb_server and open the web interface. This gets resourcepackage to generate the needed files. o Replace the __init__.py file in spambayes/spambayes/resources with a blank file to disable resourcepackage. o Ensure that the version numbers in spambayes/spambayes/__init__.py and spambayes/spambayes/Version.py are up-to-date. o Ensure that you don't have any other copies of spambayes in your PYTHONPATH, or py2exe will pick these up! If in doubt, run setup.py install. o Run the "setup_all.py" script in the spambayes/windows/py2exe/ directory. This uses py2exe to create the files that Inno will install. o Open (in InnoSetup) the spambayes.iss file in the spambayes/windows/ directory. Change the version number in the AppVerName and OutputBaseFilename lines to the new number. o Compile the spambayes.iss script to get the executable. o You can now follow the steps in the source release description above, from the testing step. Making a translation ==================== Note that it is, in general, best to translate against a stable version. This means you avoid having to repeatedly re-translate text as the code changes. This means code that has been released via the sourceforge system, that does not have a letter code at the end of the version (e.g. 1.0.1, 1.1.2, but not 1.0a1, 1.1b1, or 2.1rc2). If you do want to translate a more recent version, be sure to discuss your plans first on spambayes-dev so that you can be warned about any planned changes. Translation is only feasible for 1.1 and above. No translation effort is planned for the 1.0.x series of releases. To translate, you will need: o A suitable version of Python (2.2 or greater) installed. See http://python.org/download o A copy of the SpamBayes source that you wish to translate. o Resourcepackage installed. See http://resourcepackage.sourceforge.net Optional tools that may make translation easier include: o A copy of VC++, Visual Studio, or some other GUI tool that allows editing of VC++ dialog resource files. o A GUI HTML editor. o A GUI gettext editor, such as poEdit. http://poedit.sourceforge.net Setup ----- You will need to create a directory structure as follows: spambayes/ # spambayes package directory # containing classifier.py, tokenizer.py, etc languages/ # root languages directory, # possibly already containing # other translations {lang_code}/ # directory for the specific # translation - {lang_code} is # described below DIALOGS/ # directory for Outlook plug-in # dialog resources, which should contain an # empty __init__.py file, so that py2exe can # include the directory LC_MESSAGES/ # directory for gettext managed # strings, which should also contain an # empty __init__.py file __init__.py # Copy of spambayes/spambayes/resources/__init__.py Translation Tasks ----------------- There are four translation tasks: o Documentation. This is the least exciting, but the most important. If the documentation is appropriately translated, then even if elements of the interface are not translated, users should be able to manage. A method of managing translated documents has yet to be created. If you are interested in translating documentation, please contact spambayes-dev@python.org. o Outlook dialogs. The majority of the Outlook plug-in interface is handled by a VC++/Visual Studio dialog resource file pair (dialogs.h and dialogs.rc). The plug-in code then manipulates this to create the actual dialog. The easiest method of translating these dialogs is to use a tool like VC++ or Visual Studio. Simply open the 'Outlook2000\dialogs\resources\dialogs.rc' file, translate the dialog, and save the file as 'spambayes\languages\{lang_code}\DIALOGS\dialogs.rc', where {lang_code} is the appropriate language code for the language you have translated into (e.g. 'en_UK', 'es', 'de_DE'). If you do not have a GUI tool to edit the dialogs, simply open the dialogs.rc file in a text editor, manually change the appropriate strings, and save the file as above. Once the dialogs are translated, you need to use the rc2py.py utility to create the i18n_dialogs.py file. For example, in the 'Outlook2000\dialogs\resources' directory: > rc2py.py {base}\spambayes\languages\de_DE\DIALOGS\dialogs.rc {base}\spambayes\languages\de_DE\DIALOGS\i18n_dialogs.py 1 Where {base} is the directory that contains the spambayes package directory. This should create a 'i18n_dialogs.py' in the same directory as your translated dialogs.rc file - this is the file the the Outlook plug-in uses. o Web interface template file. The majority of the web interface is created by dynamic use of a HTML template file. The easiest method of translating this file is to use a GUI HTML editor. Simply open the 'spambayes/resources/ui.html' file, translate it as described within, and save the file as 'spambayes/languages/{lang_code}/i18n.ui.html', where {lang_code} is the appropriate language code as described above. If you do not have a GUI HTML editor, or are happy editing HTML by hand, simply use your favority HTML editor to do this task. Once the template file is created, resourcepackage will automatically create the required ui_html.py file when SpamBayes is run with that language selected. o Gettext managed strings. The remainder of both the Outlook plug-in and the web interface are contained within the various Python files that make up SpamBayes. The Python gettext module (very similar to the GNU gettext system) is used to manage translation of these strings. To translate these strings, use the translation template 'spambayes/languages/messages.pot'. You can regenerate that file, if necessary, by running this command in the spambayes package directory: > {python dir}\tools\i18n\pygettext.py -o languages\messages.pot ..\contrib\*.py ..\Outlook2000\*.py ..\scripts\*.py *.py ..\testtools\*.py ..\utilities\*.py ..\windows\*.py You may wish to use a GUI system to create the required messages.po file, such as poEdit, but you can also do this manually with a text editor. If your utility does not do it for you, you will also need to compile the .po file to a .mo file. The utility msgfmt.py will do this for you - it should be located '{python dir}\tools\i18n'. Testing the translation ----------------------- There are two ways to set the language that SpamBayes will use: o If you are using Windows, change the preferred Windows language using the Control Panel. o Get the '[globals] language' SpamBayes option to a list of the preferred language(s). spambayes-1.1a6/README.txt0000664000076500000240000005360310672653705015401 0ustar skipstaff00000000000000Copyright (C) 2002-2007 Python Software Foundation; All Rights Reserved The Python Software Foundation (PSF) holds copyright on all material in this project. You may use it under the terms of the PSF license; see LICENSE.txt. Overview ======== SpamBayes is a tool used to segregate unwanted mail (spam) from the mail you want (ham). Before SpamBayes can be your spam filter of choice you need to train it on representative samples of email you receive. After it's been trained, you use SpamBayes to classify new mail according to its spamminess and hamminess qualities. When SpamBayes filters your email, it compares each unclassified message against the information it saved from training and makes a decision about whether it thinks the message qualifies as ham or spam, or if it's unsure about how to classify the message. It then passes this information on to your mail client. Unless you are using IMAP or Outlook, this means it adds a header to each message, X-SpamBayes-Classification: spam|ham|unsure. You can then filter on this header, to file away suspected spam into its own mail folder for example. IMAP and Outlook both have the capacity to do the filtering themselves, so the header is not necessary. If you have any questions that this document does not answer, you should definitely try the SpamBayes website , and in particular, try reading the list of frequently asked questions: Prerequisites ============= You need to have Python 2.2 or later (2.3 is recommended). You can download Python from . Many distributions of UNIX now ship with Python - try typing 'python' at a shell prompt. You also need version 2.4.3 or above of the Python "email" package. If you're running Python 2.2.3 or above then you already have a good version of the email package. If not, you can download email version 2.5 from the email SIG at and install it - unpack the archive, cd to the email-2.5 directory and type "python setup.py install". This will install it into your Python site-packages directory. You'll also need to move aside the standard "email" library - go to your Python "Lib" directory and rename "email" to "email_old". To run the Outlook plug-in from source, you also need have the win32com extensions installed (win32all-149 or above), which you can get from . When installing SpamBayes on some *nix systems, such as Debian, you may need to install the python-dev package. This can be done with a command like "apt-get install python-dev" (this may vary between distributions). Getting the software ==================== If you don't already have it, you can download the latest release of SpamBayes from . For the Really Impatient ======================== If you get your mail from a POP3 server, then all you should need to do to get running is change your mail client to send and receive mail from "localhost", and then run "python setup.py install" and then "python scripts/sb_server.py -b" in the directory you expanded the SpamBayes source into. This will open a web browser window - click the "Configuration" link at the top right and fill in the various settings. Installation ============ The first thing you need to do is run "python setup.py install" in the directory that you expanded the SpamBayes archive into (to do this, you probably need to open up a console window/command prompt/DOS prompt, and navigate to the appropriate directory with the "cd" command). This will install all the files that you need into the correct locations. After this, you can delete that directory; it is no longer required. Before you begin ---------------- It's a good idea to train SpamBayes before you start using it, although this isn't compulsory. You need to save your incoming email for awhile, segregating it into two piles, known spam (bad mail) and known ham (good mail). It's best to train on recent email, because your interests and the nature of what spam looks like change over time. Once you've collected a fair portion of each (anything is better than nothing, but it helps to have a couple hundred of each), you can tell SpamBayes, "Here's my ham and my spam". It will then process that mail and save information about different patterns which appear in ham and spam. That information is then used during the filtering stage. See the "Training" section below for details. For more detailed instructions, please read the appropriate section below (if you don't know, you probably want the POP3 Proxy section). Outlook plug-in --------------- For information about how to use the Outlook plug-in, please read the "about.html" file in the Outlook2000 directory. If you want to run the Outlook plug-in from source, you should also read the "README.txt" file in that directory. POP3 Proxy ---------- You need to configure your email client to talk to the proxies instead of the real email servers. Change your equivalent of "pop3.example.com" to "localhost" (or to the name of the machine you're running the proxy on) in your email client's setup, and do the same with your equivalent of "smtp.example.com". Now launch SpamBayes, either by running "pop3proxy_service.py install" and then "net start pop3proxy" (for those using Windows 2000, Windows NT or Windows XP), or the "sb_server.py" script (for everyone else). Note that if you want to use the service, you need to also have Mark Hammond's win32 extensions for Python installed: All you need to do to configure SpamBayes is to open a web page to , click on the "Configuration" link at the top right, and fill in the relevant details. Everything should be OK with the defaults, except for the POP3 and SMTP server information at the top, which is required. For the local ports to proxy on, if you are only proxying one server, and you are using Windows, then 110 is probably the best port to try first. If that doesn't work, try using 8110 (and if you are proxying multiple ports, continue with 8111, 8112, and so on). Note that *nix users may not have permission to bind ports lower than 1025, so should choose numbers higher than that. When you check your mail in your mail client now, messages should have an addition SpamBayes header (you may not be able to see this by default). You should be able to create a mail folder called "Spam" and set up a filtering rule that puts emails with an "X-Spambayes-Classification: spam" header into that folder. Note that if you set your mail client to delete the mail without downloading the whole message (like Outlook Express's "delete from server" rule) that you may not get accurate results - the classification will be based on the headers only, not the body. This is not recommended. IMAP Filter ----------- To configure SpamBayes, run "sb_imapfilter.py -b", which should open a web page to , click on the "Configuration" link at the top right, and fill in the relevant details. Everything should be OK with the defaults, except for the server information at the top. You now need to let SpamBayes know which IMAP folders it should work with. Use the "configure folders to filter" and "configure folders to train" links on the web page to do this. The 'filter' folders are those that will have mail that you want to identify as either ham (good) or spam (bad) - this will probably be your Inbox. The 'train' folders are those that contain examples of ham and spam, to assist SpamBayes with its classification. (Folders can be used for both training and filtering). You then need to set the IMAP filter up to run periodically. At the moment, you'll need to do this from a command (or DOS) prompt. You should run the command "python sb_imapfilter.py -c -t -l 5". The '-c' means that the script should classify new mail, the '-t' means that the script should train any mail that you have told it to, and the '-l 5' means that the script should execute every five minutes (you can change this as required). XML-RPC Server -------------- The XML-RPC server (new in 1.1a4) web interface is almost identical the the POP3 proxy user interface. Instead of proxying POP3 communications though it provides an XML-RPC server your (typically non-mail) applications can use to score content submissions. To install and configure it: 1. Unpack and install the distribution: tar xvfz spambayes-1.1a4.tar.gz cd spambayes-1.1a4 python setup.py install 2. Devote a runtime directory to it: SBDIR=/usr/local/spambayes/core_server # or whatever... mkdir -p $SBDIR 3. Create an INI file: cd $SBDIR cat > bayescustomize.ini <. Follow the "Review messages" link and you'll see a list of the emails that the system has seen so far. Check the appropriate boxes and hit Train. The messages disappear and if you go back to the home page you'll see that the "Total emails trained" has increased. Alternatively, when you receive an incorrectly classified message, you can forward it to the SMTP proxy for training. If the message should have been classified as spam, forward or bounce the message to spambayes_spam@localhost, and if the message should have been classified as ham, forward it to spambayes_ham@localhost. You can still review the training through the web interface, if you wish to do so. Note that some mail clients (particularly Outlook Express) do not forward all headers when you bounce, forward or redirect mail. For these clients, you will need to use the web interface to train. Once you've done this on a few spams and a few hams, you'll find that the X-Spambayes-Classification header is getting it right most of the time. The more you train it the more accurate it gets. There's no need to train it on every message you receive, but you should train on a few spams and a few hams on a regular basis. You should also try to train it on about the same number of spams as hams. You can train it on lots of messages in one go by either using the sb_filter.py script as explained in the "Command-line training" section, or by giving messages to the web interface via the "Train" form on the Home page. You can train on individual messages (which is tedious) or using mbox files. IMAP Filter ----------- If you are running the IMAP filter with the '-t' switch, as described above, then all you need to do to train is move examples of mail into the appropriate folders, via your mail client (for example, move mail that was not classified as spam into (one of) the folder(s) that you specified as a spam training folder in the steps above. Note that training, even without any classifying, using the IMAP filter, means that your messages will be recreated (i.e. the old one is marked for deletion and a new copy is made) on the server. The messages will be identical to the original, except that they will include an additional header, so that SpamBayes can keep track of which messages have already been processed. Command-line training --------------------- Given a pair of Unix mailbox format files (each message starts with a line which begins with 'From '), one containing nothing but spam and the other containing nothing but ham, you can train Spambayes using a command like sb_mboxtrain.py -g ~/tmp/newham -s ~/tmp/newspam The above command is command-line-centric (eg. UNIX, or Windows command prompt). You can also use the web interface for training as detailed above. Overview ======== [This section will tell you more about how and what SpamBayes is, but does not contain any additional information about setting it up.] There are eight main components to the SpamBayes system: o A database. Loosely speaking, this is a collection of words and associated spam and ham probabilities. The database says "If a message contains the word 'Viagra' then there's a 98% chance that it's spam, and a 2% chance that it's ham." This database is created by training - you give it messages, tell it whether those messages are ham or spam, and it adjusts its probabilities accordingly. How to train it is covered below. By default it lives in a file called "hammie.db". o The tokenizer/classifier. This is the core engine of the system. The tokenizer splits emails into tokens (words, roughly speaking), and the classifier looks at those tokens to determine whether the message looks like spam or not. You don't use the tokenizer/classifier directly - it powers the other parts of the system. o The POP3 proxy. This sits between your email client (Eudora, Outlook Express, etc) and your incoming email server, and adds the classification header to emails as you download them. A typical user's email setup looks like this: +-----------------+ +-------------+ | Outlook Express | Internet or intranet | | | (or similar) | <--------------------------> | POP3 server | | | | | +-----------------+ +-------------+ The POP3 server runs either at your ISP for Internet mail, or somewhere on your internal network for corporate mail. The POP3 proxy sits in the middle and adds the classification header as you retrieve your email: +-----------------+ +------------+ +-------------+ | Outlook Express | | SpamBayes | | | | (or similar) | <----> | POP3 proxy | <----> | POP3 server | | | | | | | +-----------------+ +------------+ +-------------+ So where you currently have your email client configured to talk to say, "pop3.my-isp.com", you instead configure the *proxy* to talk to "pop3.my-isp.com" and configure your email client to talk to the proxy. The POP3 proxy can live on your PC, or on the same machine as the POP3 server, or on a different machine entirely, it really doesn't matter. Say it's living on your PC, you'd configure your email client to talk to "localhost". You can configure the proxy to talk to multiple POP3 servers, if you have more than one email account. o The SMTP proxy. This sits between your email client (Eudora, Outlook Express, etc) and your outgoing email server. Any mail sent to SpamBayes_spam@localhost or SpamBayes_ham@localhost is intercepted and trained appropriately. A typical user's email setup looks like this: +-----------------+ +-------------+ | Outlook Express | Internet or intranet | | | (or similar) | <--------------------------> | SMTP server | | | | | +-----------------+ +-------------+ The SMTP server runs either at your ISP for Internet mail, or somewhere on your internal network for corporate mail. The SMTP proxy sits in the middle and checks for mail to train on as you send your email: +-----------------+ +------------+ +-------------+ | Outlook Express | | SpamBayes | | | | (or similar) | <----> | SMTP proxy | <----> | SMTP server | | | | | | | +-----------------+ +------------+ +-------------+ So where you currently have your email client configured to talk to say, "smtp.my-isp.com", you instead configure the *proxy* to talk to "smtp.my-isp.com" and configure your email client to talk to the proxy. The SMTP proxy can live on your PC, or on the same machine as the SMTP server, or on a different machine entirely, it really doesn't matter. Say it's living on your PC, you'd configure your email client to talk to "localhost". You can configure the proxy to talk to multiple SMTP servers, if you have more than one email account. o The web interface. This is a server that runs alongside the POP3 proxy, SMTP proxy, and IMAP filter (see below) and lets you control it through the web. You can upload emails to it for training or classification, query the probabilities database ("How many of my emails really *do* contain the word Viagra"?), find particular messages, and most importantly, train it on the emails you've received. When you start using the system, unless you train it using the sb_filter script it will classify most things as Unsure, and often make mistakes. But it keeps copies of all the email's its seen, and through the web interface you can train it by going through a list of all the emails you've received and checking a Ham/Spam box next to each one. After training on a few messages (say 20 spams and 20 hams), you'll find that it's getting it right most of the time. The web training interface automatically checks the Ham/Spam boxes according to what it thinks, so all you need to do it correct the odd mistake - it's very quick and easy. o The Outlook plug-in. For Outlook 2000 and Outlook XP users (not Outlook Express) this lets you manage the whole thing from within Outlook. You set up a Ham folder and a Spam folder, and train it simply by dragging messages into those folders. Alternatively there are buttons to do the same thing. And it integrates into Outlook's filtering system to make it easy to file all the suspected spam into its own folder, for instance. o The sb_filter.py script. This does three jobs: command-line training, procmail filtering, and XML-RPC. See below for details of how to use sb_filter for training, and how to use it as procmail filter. You can also run an XML-RPC server, so that a programmer can write code that uses a remote server to classify emails programmatically - see sb_xmlrpcserver.py. o The IMAP filter. This is a cross between the POP3 proxy and the Outlook plugin. If your mail sits on an IMAP server, you can use the this to filter your mail. You can designate folders that contain mail to train as ham and folders that contain mail to train as spam, and the filter does this for you. You can also designate folders to filter, along with a folder for messages SpamBayes is unsure about, and a folder for suspected spam. When new mail arrives, the filter will move mail to the appropriate location (ham is left in the original folder). spambayes-1.1a6/runtest.sh0000775000076500000240000000376310646440140015734 0ustar skipstaff00000000000000#! /bin/sh -e ## ## runtest.sh -- run some tests for Tim ## ## This does everything you need to test yer data. You may want to skip ## the rebal steps if you've recently moved some of your messages ## (because they were in the wrong corpus) or you may suffer my fate and ## get stuck forever re-categorizing email. ## ## Just set up your messages as detailed in README.txt; put them all in ## the reservoir directories, and this script will take care of the ## rest. Paste the output to the mailing list for good karma. ## ## Neale Pickett ## if [ "$1" = "-r" ]; then REBAL=1 shift fi # Include local directory in Python path if [ -n "$PYTHONPATH" ]; then PYTHONPATH=$PYTHONPATH:. else PYTHONPATH=. fi export PYTHONPATH # Which test to run TEST=${1:-run2} # Number of messages per rebalanced set RNUM=${REBAL_RNUM:-200} # Number of sets case ${REBAL_SETS:-undefined} in undefined) # count the number of sets i=1 while [ -d Data/Ham/Set$i -a -d Data/Spam/Set$i ]; do i=`expr $i + 1` done SETS=`expr $i - 1` ;; *) # use the provided value SETS=${REBAL_SETS} ;; esac set -x if [ -n "$REBAL" ]; then # Put them all into reservoirs python utilities/rebal.py -r Data/Ham/reservoir -s Data/Ham/Set -n 0 -q python utilities/rebal.py -r Data/Spam/reservoir -s Data/Spam/Set -n 0 -q # Rebalance python utilities/rebal.py -r Data/Ham/reservoir -s Data/Ham/Set -n $RNUM -q -Q python utilities/rebal.py -r Data/Spam/reservoir -s Data/Spam/Set -n $RNUM -q -Q fi case "$TEST" in test1) python testtools/timtest.py -n $SETS > test1.txt ;; test2) python testtools/timtest.py -n $SETS > test2.txt ;; timcv1|cv1) python testtools/timcv.py -n $SETS > cv1.txt ;; timcv2|cv2) python testtools/timcv.py -n $SETS > cv2.txt python testtools/rates.py cv1 cv2 > runrates.txt python testtools/cmp.py cv1s cv2s | tee results.txt ;; *) echo "Available targets:" sed -n 's/^\( *[a-z0-9|]*\))$/\1/p' $0 ;; esac spambayes-1.1a6/scripts/0000775000076500000240000000000011355064626015360 5ustar skipstaff00000000000000spambayes-1.1a6/scripts/core_server.py0000664000076500000240000001440011116610271020233 0ustar skipstaff00000000000000#!/usr/bin/env python """The primary server for SpamBayes. Currently serves the web interface only. Plugging in listeners for various protocols is TBD. This is a first cut at creating a standalone server which uses a plugin architecture to support different protocols. The primary motivation is that web apps like MoinMoin, Trac and Roundup can use spam detection, but they don't necessarily provide the mechanisms necessary to save ham and spam databases for retraining. By providing protocol plugins you should be able to fairly easily provide (for example) an XML-RPC interface web apps can use. The core server takes care of all the training bells and whistles. Usage: core_server.py [options] options: -h : Displays this help message. -P module : Identify plugin module to use (required) -d FILE : use the named DBM database file -p FILE : the the named Pickle database file -u port : User interface listens on this port number (default 8880; Browse http://localhost:8880/) -b : Launch a web browser showing the user interface. -o section:option:value : set [section, option] in the options database to value All command line arguments and switches take their default values from the [html_ui] section of bayescustomize.ini. """ # This module is part of the spambayes project, which is Copyright 2002-2007 # The Python Software Foundation and is covered by the Python Software # Foundation license. __author__ = "Richie Hindle " __credits__ = "Tim Peters, Neale Pickett, Tim Stone, all the Spambayes folk." _TODO = """ Protocol plugin interface: o Classifier for web apps (e.g. Trac, Roundup, Moin) o POP3? o NNTP? Web training interface: User interface improvements: o Once the pieces are on separate pages, make the paste box bigger. o Deployment: Windows executable? atlaxwin and ctypes? Or just webbrowser? o "Reload database" button. New features: o Online manual. o Links to project homepage, mailing list, etc. o List of words with stats (it would have to be paged!) a la SpamSieve. Info: o Slightly-wordy index page; intro paragraph for each page. o In both stats and training results, report nham and nspam. o "Links" section (on homepage?) to project homepage, mailing list, etc. Gimmicks: o Graphs. Of something. Who cares what? """ import sys, getopt from spambayes import Dibbler from spambayes.Options import options, _ from spambayes.UserInterface import UserInterfaceServer from spambayes.Version import get_current_version from spambayes.CoreUI import CoreUserInterface, CoreState, \ AlreadyRunningException # Increase the stack size on MacOS X. Stolen from Lib/test/regrtest.py if sys.platform == 'darwin': try: import resource except ImportError: pass else: soft, hard = resource.getrlimit(resource.RLIMIT_STACK) newsoft = min(hard, max(soft, 1024*2048)) resource.setrlimit(resource.RLIMIT_STACK, (newsoft, hard)) # Option-parsing helper functions def _addressAndPort(s): """Decode a string representing a port to bind to, with optional address.""" s = s.strip() if ':' in s: addr, port = s.split(':') return addr, int(port) else: return '', int(s) def _addressPortStr((addr, port)): """Encode a string representing a port to bind to, with optional address.""" if not addr: return str(port) else: return '%s:%d' % (addr, port) def load_plugin(name, state): try: plugin_module = __import__(name) except ImportError: plugin_module = __import__("spambayes.%s" % name) plugin_module = getattr(plugin_module, name) plugin = plugin_module.register() plugin.state = state return plugin def main(state): """Runs the core server forever or until a 'KILL' command is received or someone hits Ctrl+Break.""" http_server = UserInterfaceServer(state.ui_port) http_server.register(CoreUserInterface(state)) Dibbler.run(launchBrowser=state.launch_ui) # =================================================================== # __main__ driver. # =================================================================== def run(): # Read the arguments. try: opts, args = getopt.getopt(sys.argv[1:], 'hbd:p:l:u:o:P:') except getopt.error, msg: print >> sys.stderr, str(msg) + '\n\n' + __doc__ sys.exit() state = CoreState() state.plugin = None for opt, arg in opts: if opt == '-h': print >> sys.stderr, __doc__ sys.exit() elif opt == '-b': state.launch_ui = True # '-p' and '-d' are handled by the storage.database_type call # below, in case you are wondering why they are missing. elif opt == '-l': state.proxyPorts = [_addressAndPort(a) for a in arg.split(',')] elif opt == '-u': state.ui_port = int(arg) elif opt == '-o': options.set_from_cmdline(arg, sys.stderr) elif opt == '-P': state.plugin = load_plugin(arg, state) if state.plugin is None: print >> sys.stderr, "No plugin argument (-P) was given." print >> sys.stderr, __doc__ sys.exit() # Let the user know what they are using... v = get_current_version() print "%s\n" % (v.get_long_version("SpamBayes Core Proxy"),) if 0 <= len(args) <= 2: # Normal usage, with optional server name and port number. if len(args) == 1: state.servers = [(args[0], 110)] elif len(args) == 2: state.servers = [(args[0], int(args[1]))] try: state.prepare() except AlreadyRunningException: print >> sys.stderr, \ "ERROR: The proxy is already running on this machine." print >> sys.stderr, "Please stop the existing proxy and try again" return # kick everything off try: main(state) finally: state.close() else: print >> sys.stderr, __doc__ if __name__ == '__main__': try: run() except KeyboardInterrupt: print "bye!" spambayes-1.1a6/scripts/README.txt0000664000076500000240000000052510646440131017047 0ustar skipstaff00000000000000This directory contains a collection of spambayes applications/scripts. Each file within this directory should be executable, and perform a specific task (export the database, launch a POP3 proxy, and so on). To avoid polluting the end user's python/scripts directory when spambayes is installed, each script should be prefixed with 'sb_'.spambayes-1.1a6/scripts/sb_bnfilter.py0000664000076500000240000001770211112670636020225 0ustar skipstaff00000000000000#! /usr/bin/env python # This script has a similar interface and purpose to sb_filter, but avoids # re-initialising spambayes for consecutive requests using a short-lived # server process. This is intended to give the performance advantages of # sb_xmlrpcserver, without the administrative complications. # # The strategy is: # # * while we cant connect to a unix domain socket # * fork a separate process that runs in the background # * in the child process: # * exec sb_bnserver. it listens on that same unix domain socket. # * in the parent process: # * sleep a little, to give the child chance to start up # * write the filtering/training command line options to the socket # * copy the content of stdin to the socket # * meanwhile..... sb_bnserver gets to work on that data in the same manner # as sb_filter. it writes its response back through that socket # * read a line from the socket containing a success/failure code # * read a line from the socket containing a byte count # * copy the remainder of the content of the socket to stdout or stderr, # depending on whether it reported success or failure. # * if the number of bytes read from the socket is different to the byte # count, exit with an error # * if the reported exit code is non-zero, exit with an error # # sb_bnfilter will only terminate with a zero exit code if everything # is ok. If it terminates with a non-zero exit code then its stdout should # be ignored. # # sb_bnserver will close itself and remove its socket after a period of # inactivity to ensure it does not use up resources indefinitely. # # Author: Toby Dickenson # """Usage: %(program)s [options] Where: -h show usage and exit * -f filter (default if no processing options are given) * -g [EXPERIMENTAL] (re)train as a good (ham) message * -s [EXPERIMENTAL] (re)train as a bad (spam) message * -t [EXPERIMENTAL] filter and train based on the result -- you must make sure to untrain all mistakes later. Not recommended. * -G [EXPERIMENTAL] untrain ham (only use if you've already trained this message) * -S [EXPERIMENTAL] untrain spam (only use if you've already trained this message) -k FILE Unix domain socket used to communicate with a short-lived server process. Default is ~/.sbbnsock- These options will not take effect when connecting to a preloaded server: -p FILE use pickle FILE as the persistent store. loads data from this file if it exists, and saves data to this file at the end. -d FILE use DBM store FILE as the persistent store. -o section:option:value set [section, option] in the options database to value -a seconds timeout in seconds between requests before this server terminates -A number terminate this server after this many requests """ import sys, getopt, socket, errno, os, time def usage(code, msg=''): """Print usage message and sys.exit(code).""" if msg: print >> sys.stderr, msg print >> sys.stderr print >> sys.stderr, __doc__ sys.exit(code) def main(): try: opts, args = getopt.getopt(sys.argv[1:], 'hfgstGSd:p:o:a:A:k:') except getopt.error, msg: usage(2, msg) # build the default socket filename from environment variables filename = os.path.expanduser('~/.sbbnsock-'+socket.gethostname()) action_options = [] server_options = [] for opt, arg in opts: if opt == '-h': usage(0) elif opt in ('-f', '-g', '-s', '-t', '-G', '-S'): action_options.append(opt) elif opt in ('-d', '-p', '-o', '-a', '-A'): server_options.append(opt) server_options.append(arg) elif opt == '-k': filename = arg if args: usage(2) server_options.append(filename) s = make_socket(server_options, filename) # We have a connection to the existing shared server w_file = s.makefile('w') r_file = s.makefile('r') # pass our command line on the first line into the socket w_file.write(' '.join(action_options)+'\n') # copy entire contents of stdin into the socket while 1: b = sys.stdin.read(1024*64) if not b: break w_file.write(b) w_file.flush() w_file.close() s.shutdown(1) # expect to get back a line containing the size of the rest of the response error = int(r_file.readline()) expected_size = int(r_file.readline()) if error: output = sys.stderr else: output = sys.stdout total_size = 0 # copy entire contents of socket into stdout or stderr while 1: b = r_file.read(1024*64) if not b: break output.write(b) total_size += len(b) output.flush() # If we didnt receive the right amount then something has gone wrong. # exit now, and procmail will ignore everything we have sent to stdout. # Note that this policy is different to the xmlrpc client, which # tries to handle errors internally by constructing a stdout that is # the same as stdin was. if total_size != expected_size: print >> sys.stderr, 'size mismatch %d != %d' % (total_size, expected_size) sys.exit(3) if error: sys.exit(error) def make_socket(server_options, filename): refused_count = 0 no_server_count = 0 while 1: try: s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.connect(filename) except socket.error,e: if e[0] == errno.EAGAIN: # baaah pass elif e[0] == errno.ENOENT or not os.path.exists(filename): # We need to check os.path.exists for use on operating # systems that never return ENOENT; linux 2.2. # # no such file.... no such server. create one. no_server_count += 1 if no_server_count > 4: raise # Reset refused count to start the sleep process over. # Otherwise we run the risk of waiting a *really* long time # and/or hitting the refused_count limit. refused_count = 0 fork_server(server_options) elif e[0] == errno.ECONNREFUSED: # socket file exists but noone listening. refused_count += 1 if refused_count == 4: # We have been waiting ages and still havent been able # to connect. Maybe that socket file has got # orphaned. remove it, wait, and try again. We need to # allow enough time for sb_bnserver to initialise the # rest of spambayes try: os.unlink(filename) except EnvironmentError: pass elif refused_count > 6: raise else: raise # some other problem time.sleep(0.2 * 2.0**no_server_count * 2.0**refused_count) else: return s def fork_server(options): if os.fork(): # parent return os.close(0) sys.stdin = sys.__stdin__ = open("/dev/null") os.close(1) sys.stdout = sys.__stdout__ = open("/dev/null", "w") # leave stderr # os.close(2) # sys.stderr = sys.__stderr__ = open("/dev/null", "w") os.setsid() # Use exec rather than import here because eventually it may be nice to # reimplement this one file in C os.execv(sys.executable, [sys.executable, os.path.join(os.path.split(sys.argv[0])[0], 'sb_bnserver.py') ]+options) # should never get here sys._exit(1) if __name__ == "__main__": main() spambayes-1.1a6/scripts/sb_bnserver.py0000664000076500000240000001206111116563043020234 0ustar skipstaff00000000000000#! /usr/bin/env python # Another server version of hammie.py # This is not intended to be run manually, it is the opportunistic # daemon backend of sb_bnfilter. # # Author: Toby Dickenson # """Usage: %(program)s [options] FILE Where: -h show usage and exit -p FILE use pickle FILE as the persistent store. loads data from this file if it exists, and saves data to this file at the end. -d FILE use DBM store FILE as the persistent store. -o section:option:value set [section, option] in the options database to value -a seconds timeout in seconds between requests before this server terminates -A number terminate this server after this many requests FILE unix domain socket used on which we listen """ import os, getopt, sys, SocketServer, traceback, select, socket, errno # See Options.py for explanations of these properties program = sys.argv[0] def usage(code, msg=''): """Print usage message and sys.exit(code).""" if msg: print >> sys.stderr, msg print >> sys.stderr print >> sys.stderr, __doc__ sys.exit(code) def main(): """Main program; parse options and go.""" try: opts, args = getopt.getopt(sys.argv[1:], 'hd:p:o:a:A:') except getopt.error, msg: usage(2, msg) if len(args) != 1: usage(2, "socket not specified") # get the server up before initializing spambayes, so that # we haven't wasted time if we later find we can't start the server try: server = BNServer(args[0], BNRequest) except socket.error,e: if e[0] == errno.EADDRINUSE: pass # in use, no need else: raise # a real error else: try: from spambayes import Options, storage options = Options.options for opt, arg in opts: if opt == '-h': usage(0) elif opt == '-o': options.set_from_cmdline(arg, sys.stderr) elif opt == '-a': server.timeout = float(arg) elif opt == '-A': server.number = int(arg) h = make_HammieFilter() h.dbname, h.usedb = storage.database_type(opts) server.hammie = h server.serve_until_idle() h.close() finally: try: os.unlink(args[0]) except EnvironmentError: pass class NowIdle(Exception): pass class BNServer(SocketServer.UnixStreamServer): allow_reuse_address = True timeout = 10.0 number = 100 def serve_until_idle(self): try: for i in range(self.number): self.handle_request() except NowIdle: pass def get_request(self): r, w, e = select.select([self.socket], [], [], self.timeout) if r: return self.socket.accept() else: raise NowIdle() class BNRequest(SocketServer.StreamRequestHandler): def handle(self): switches = self.rfile.readline() body = self.rfile.read() try: response = self._calc_response(switches, body) self.wfile.write('0\n%d\n'%(len(response),)) self.wfile.write(response) except: response = traceback.format_exception_only(sys.exc_info()[0], sys.exc_info()[1])[0] self.wfile.write('1\n%d\n'%(len(response),)) self.wfile.write(response) def _calc_response(self, switches, body): switches = switches.split() actions = [] opts, args = getopt.getopt(switches, 'fgstGS') h = self.server.hammie for opt, arg in opts: if opt == '-f': actions.append(h.filter) elif opt == '-g': actions.append(h.train_ham) elif opt == '-s': actions.append(h.train_spam) elif opt == '-t': actions.append(h.filter_train) elif opt == '-G': actions.append(h.untrain_ham) elif opt == '-S': actions.append(h.untrain_spam) if actions == []: actions = [h.filter] from spambayes import mboxutils msg = mboxutils.get_message(body) for action in actions: action(msg) return mboxutils.as_string(msg, 1) def make_HammieFilter(): # The sb_hammie script has some logic in the HammieFiler class that we need here too. # Ideally that should be moved into the spambayes package, but for now lets just # abuse sys.path, make assumptions about the directory layout, and import it direct # from the sb_filter script. from spambayes import Options path = os.path.split(Options.__file__)[0]+'/../scripts' if path not in sys.path: sys.path.append(path) from sb_filter import HammieFilter return HammieFilter() if __name__ == "__main__": main() spambayes-1.1a6/scripts/sb_chkopts.py0000664000076500000240000000023710646440131020062 0ustar skipstaff00000000000000#!/usr/bin/env python """ Trivial script to check that the user's options file doesn't contain any old-style option names. """ from spambayes import Options spambayes-1.1a6/scripts/sb_client.py0000664000076500000240000000112111112670644017661 0ustar skipstaff00000000000000#! /usr/bin/env python """A client for sb_xmlrpcserver.py. Just feed it your mail on stdin, and it spits out the same message with the spambayes score in a new X-Spambayes-Disposition header. """ import xmlrpclib import sys RPCBASE = "http://localhost:65000" def main(): msg = sys.stdin.read() try: x = xmlrpclib.ServerProxy(RPCBASE) m = xmlrpclib.Binary(msg) out = x.filter(m) print out.data except: if __debug__: import traceback traceback.print_exc() print msg if __name__ == "__main__": main() spambayes-1.1a6/scripts/sb_dbexpimp.py0000664000076500000240000001556211116631711020225 0ustar skipstaff00000000000000#! /usr/bin/env python """sb_dbexpimp.py - Bayes database export/import This utility has the primary function of exporting and importing a spambayes database into/from a CSV file. This is useful in a number of scenarios. Platform portability of database - CSV files can be exported and imported across platforms (Windows and Linux, for example). Database implementation changes - databases can survive database implementation upgrades or new database implementations. For example, if a dbm implementation changes between python x.y and python x.y+1... Database reorganization - an export followed by an import reorgs an existing database, improving performance, at least in some database implementations. Database sharing - it is possible to distribute particular databases for research purposes, database sharing purposes, or for new users to have a 'seed' database to start with. Database merging - multiple databases can be merged into one quite easily by specifying -m on an import. This will add the two database nham and nspams together and for wordinfo conflicts, will add spamcount and hamcount together. Usage: sb_dbexpimp [options] options: -e : export -i : import -f: FN : flat file to export to or import from -p: FN : name of pickled database file to use -d: FN : name of dbm database file to use -m : merge import into an existing database file. This is meaningful only for import. If omitted, a new database file will be created. If specified, the imported wordinfo will be merged into an existing database. Run dbExpImp -h for more information. -o: section:option:value : set [section, option] in the options database to value -h : help If neither -p nor -d is specified, then the values in your configuration file (or failing that, the defaults) will be used. In this way, you may convert to and from storage formats other than pickle and dbm. Examples: Export pickled mybayes.db into mybayes.db.export as a CSV file sb_dbexpimp -e -p mybayes.db -f mybayes.db.export Import mybayes.db.export into a new DBM mybayes.db sb_dbexpimp -i -d mybayes.db -f mybayes.db.export Convert a bayes database from pickle to DBM sb_dbexpimp -e -p abayes.db -f abayes.export sb_dbexpimp -i -d abayes.db -f abayes.export Create a new DBM database (newbayes.db) from two DBM databases (abayes.db, bbayes.db) sb_dbexpimp -e -d abayes.db -f abayes.export sb_dbexpimp -e -d bbayes.db -f bbayes.export sb_dbexpimp -i -d newbayes.db -f abayes.export sb_dbexpimp -i -m -d newbayes.db -f bbayes.export """ # This module is part of the spambayes project, which is Copyright 2002-2007 # The Python Software Foundation and is covered by the Python Software # Foundation license. from __future__ import generators __author__ = "Tim Stone " import csv import spambayes.storage from spambayes.Options import options import sys, os, getopt, errno from types import UnicodeType def uquote(s): if isinstance(s, UnicodeType): s = s.encode('utf-8') return s # Heaven only knows what encoding non-ASCII stuff will be in # Try a few common western encodings and punt if they all fail def uunquote(s): for encoding in ("utf-8", "cp1252", "iso-8859-1"): try: return unicode(s, encoding) except UnicodeDecodeError: pass # punt return s def runExport(dbFN, useDBM, outFN): bayes = spambayes.storage.open_storage(dbFN, useDBM) if useDBM == "dbm": words = bayes.db.keys() words.remove(bayes.statekey) else: words = bayes.wordinfo.keys() try: fp = open(outFN, 'wb') except IOError, e: if e.errno != errno.ENOENT: raise writer = csv.writer(fp) nham = bayes.nham nspam = bayes.nspam print "Exporting database %s to file %s" % (dbFN, outFN) print "Database has %s ham, %s spam, and %s words" \ % (nham, nspam, len(words)) writer.writerow([nham, nspam]) for word in words: wi = bayes._wordinfoget(word) hamcount = wi.hamcount spamcount = wi.spamcount word = uquote(word) writer.writerow([word, hamcount, spamcount]) def runImport(dbFN, useDBM, newDBM, inFN): if newDBM: try: os.unlink(dbFN) except OSError: pass bayes = spambayes.storage.open_storage(dbFN, useDBM) fp = open(inFN, 'rb') rdr = csv.reader(fp) (nham, nspam) = rdr.next() if newDBM: bayes.nham = int(nham) bayes.nspam = int(nspam) else: bayes.nham += int(nham) bayes.nspam += int(nspam) if newDBM: impType = "Importing" else: impType = "Merging" print "%s file %s into database %s" % (impType, inFN, dbFN) for (word, hamcount, spamcount) in rdr: word = uunquote(word) # Can't use wordinfo[word] here, because wordinfo # is only a cache with dbm! Need to use _wordinfoget instead. wi = bayes._wordinfoget(word) if wi is None: wi = bayes.WordInfoClass() wi.hamcount += int(hamcount) wi.spamcount += int(spamcount) bayes._wordinfoset(word, wi) print "Storing database, please be patient. Even moderately sized" print "databases may take a very long time to store." bayes.store() print "Finished storing database" if useDBM == "dbm" or useDBM == True: words = bayes.db.keys() words.remove(bayes.statekey) else: words = bayes.wordinfo.keys() print "Database has %s ham, %s spam, and %s words" \ % (bayes.nham, bayes.nspam, len(words)) if __name__ == '__main__': try: opts, args = getopt.getopt(sys.argv[1:], 'iehmvd:p:f:o:') except getopt.error, msg: print >> sys.stderr, str(msg) + '\n\n' + __doc__ sys.exit() useDBM = "pickle" newDBM = True dbFN = None flatFN = None exp = False imp = False for opt, arg in opts: if opt == '-h': print >> sys.stderr, __doc__ sys.exit() elif opt == '-f': flatFN = arg elif opt == '-e': exp = True elif opt == '-i': imp = True elif opt == '-m': newDBM = False elif opt in ('-o', '--option'): options.set_from_cmdline(arg, sys.stderr) dbFN, useDBM = spambayes.storage.database_type(opts) if (dbFN and flatFN): if exp: runExport(dbFN, useDBM, flatFN) if imp: runImport(dbFN, useDBM, newDBM, flatFN) else: print >> sys.stderr, __doc__ spambayes-1.1a6/scripts/sb_evoscore.py0000775000076500000240000000767310672654047020266 0ustar skipstaff00000000000000#! /usr/bin/env python # Copyright (C) 2003-2007 Python Software Foundation; All Rights Reserved # # Licensed under the Python Software Foundation License, which you should have # received as part of the Spambayes distribution. # # Author: Barry Warsaw """A shim for integrating Spambayes and Ximian Evolution. Evolution is a free email client for Linux and Solaris, developed by Ximian. See www.ximian.com for details. Evolution is sometimes called 'Evo' for short. Actually Evo is more than that -- you can think of it as an Outlook-alike for Linux -- but all we care about here is the mail reader. Evo can connect to a POP or IMAP server, so you can of course use Spambayes' normal POP or IMAP filters. For a variety of reasons, I don't like hooking things up this way. In Evo, you can specify filters which run on folders whenever a message shows up in that folder. The filter can have any number of criteria, and if the criteria match, Evo will execute some number of actions. One of the things a filter can do is pipe the message to a program's standard in, and then check the exit code of that program. That's how we'll hook things together. You'll need to start sb_xmlrpcserver.py, as this script is a client of that server. You'll use sb_imapfilter.py to train a database on the machine local to your Evo client (yes, if you use many different workstations, you'll need your database on all of them). sb_evoscore.py takes the message from standard in, sends it to the xmlrpc server and receives the float spam score for the message. Then it compares this to your ham_cutoff and spam_cutoff options. It exits with a return code that your Evo filter will check. The return codes are: -1 - Error 0 - Ham 1 - Unsure 2 - Spam So, to hook things up do the following: - In Evo, go to Tools -> Filters... to bring up the filter rules - Select 'incoming' as the filter rule direction - Add a new filter rule called 'Spambayes Spam' - Select 'Pipe Message to Shell Command' as the first and only criteria. Point the command at this script, e.g. /usr/local/bin/sb_evoscore.py - Select the match criteria to be 'returns 2' - In the 'Then' section, select what you want to have happen for Spam. Personally, I have a folder called SBInbox, and inside that folder I have four subfolders: HamTrain, SpamTrain, Spam, and Unsure. My action for the 'Spambayes Spam' filter is then 'Move to Folder SBInbox/Spam'. - Now do the same thing with a second filter rule called 'Spambayes Unsure', except this time, match a return value of 1, and move these messages to SBInbox/Unsure. - Finally, make sure Evo will run filters automatically when your inbox receives new messages. Now, what I do is throw a bunch of known ham in SBInbox/HamTrain and a bunch of known spam in SBInbox/SpamTrain. I use 'sb_imapfilter -t -v -p' to train a database on my local machine. Then I start up sb_xmlrpcserver.py. NOTE: you must edit the variable RPCURL below to match how you invoke sb_xmlrpcserver.py. For a while, I watch the Unsure folder, moving mistakes to SpamTrain and HamTrain respectively. Every once in a while, I retrain my database, and copy my database to all my other desktops. One caveat: I've found that if I kill the xmlrpc server while Evo is still running, it can cause Evo to hang, choke, or start spewing endless error messages. It's best to exit Evo before killing sb_xmlrpcserver.py. """ import sys import xmlrpclib from spambayes.Options import options RPCURL = 'http://localhost:8881' def main(): msg = sys.stdin.read() try: server = xmlrpclib.ServerProxy(RPCURL) score = server.score(xmlrpclib.Binary(msg)) except: import traceback traceback.print_exc() return -1 else: if score < options['Categorization', 'ham_cutoff']: return 0 elif score < options['Categorization', 'spam_cutoff']: return 1 return 2 status = main() sys.exit(status) spambayes-1.1a6/scripts/sb_filter.py0000664000076500000240000002030711116563051017674 0ustar skipstaff00000000000000#!/usr/bin/env python ## A hammie front-end to make the simple stuff simple. ## ## ## The intent is to call this from procmail and its ilk like so: ## ## :0 fw ## | sb_filter.py ## ## Then, you can set up your MUA to pipe ham and spam to it, one at a ## time, by calling it with either the -g or -s options, respectively. ## ## Author: Neale Pickett ## """Usage: %(program)s [options] [filenames] Options can one or more of: -h show usage and exit -v show version and exit -x show some usage examples and exit -d DBFILE use database in DBFILE -p PICKLEFILE use pickle (instead of database) in PICKLEFILE -n create a new database * -f filter (default if no processing options are given) * -g (re)train as a good (ham) message * -s (re)train as a bad (spam) message * -t filter and train based on the result -- you must make sure to untrain all mistakes later. Not recommended. * -G untrain ham (only use if you've already trained this message) * -S untrain spam (only use if you've already trained this message) -o section:option:value set [section, option] in the options database to value -P Run under control of the Python profiler, if it is available All options marked with '*' operate on stdin, and write the resultant message to stdout. If no filenames are given on the command line, standard input will be processed as a single message. If one or more filenames are given on the command line, each will be processed according to the following rules: * If the filename is '-', standard input will be processed as a single message (may only be usefully given once). * If the filename starts with '+' it will be processed as an MH folder. * If the filename is a directory and it contains a subdirectory named 'cur', it will be processed as a Maildir. * If the filename is a directory and it contains a subdirectory named 'Mail', it will be processed as an MH Mailbox. * If the filename is a directory and not a Maildir nor an MH Mailbox, it will be processed as a Mailbox directory consisting of just .txt and .lorien files. * Otherwise, the filename is treated as a Unix-style mailbox (messages begin on a line starting with 'From '). Output is always to standard output as a Unix-style mailbox. """ import os import sys import getopt from spambayes import hammie, Options, mboxutils, storage from spambayes.Version import get_current_version # See Options.py for explanations of these properties program = sys.argv[0] example_doc = """_Examples_ filter a message on disk: %(program)s < message (re)train a message as ham: %(program)s -g < message (re)train a message as spam: %(program)s -s < message procmail recipe to filter and train in one step: :0 fw | %(program)s -t mutt configuration: This binds the 'H' key to retrain the message as ham, and prompt for a folder to move it to. The 'S' key retrains as spam, and moves to a 'spam' folder. See contrib/muttrc in the spambayes distribution for other neat mutt tricks. macro index S "|sb_filter.py -s | procmail\n" macro pager S "|sb_filter.py -s | procmail\n" macro index H "|sb_filter.py -g | procmail\n" macro pager H "|sb_filter.py -g | procmail\n" color index red black "~h 'X-Spambayes-Disposition: spam' ~F" """ def examples(): print example_doc % globals() sys.exit(0) def usage(code, msg=''): """Print usage message and sys.exit(code).""" # Include version info in usage v = get_current_version() print >> sys.stderr, v.get_long_version("SpamBayes Command Line Filter") print >> sys.stderr if msg: print >> sys.stderr, msg print >> sys.stderr print >> sys.stderr, __doc__ % globals() sys.exit(code) def version(): v = get_current_version() print >> sys.stderr, v.get_long_version("SpamBayes Command Line Filter") sys.exit(0) class HammieFilter(object): def __init__(self): options = Options.options # This is a bit of a hack to counter the default for # persistent_storage_file changing from ~/.hammiedb to hammie.db # This will work unless a user: # * had hammie.db as their value for persistent_storage_file, and # * their config file was loaded by Options.py. if options["Storage", "persistent_storage_file"] == \ options.default("Storage", "persistent_storage_file"): options["Storage", "persistent_storage_file"] = \ "~/.hammiedb" options.merge_files(['/etc/hammierc', os.path.expanduser('~/.hammierc')]) self.dbname, self.usedb = storage.database_type([]) self.mode = self.h = None def open(self, mode): if self.h is None or self.mode != mode: if self.h is not None: if self.mode != 'r': self.h.store() self.h.close() self.mode = mode self.h = hammie.open(self.dbname, self.usedb, self.mode) def close(self): if self.h is not None: if self.mode != 'r': self.h.store() self.h.close() self.h = None __del__ = close def newdb(self): self.open('n') self.close() def filter(self, msg): if Options.options["Hammie", "train_on_filter"]: self.open('c') else: self.open('r') return self.h.filter(msg) def filter_train(self, msg): self.open('c') return self.h.filter(msg, train=True) def train_ham(self, msg): self.open('c') self.h.train_ham(msg, Options.options["Headers", "include_trained"]) self.h.store() def train_spam(self, msg): self.open('c') self.h.train_spam(msg, Options.options["Headers", "include_trained"]) self.h.store() def untrain_ham(self, msg): self.open('c') self.h.untrain_ham(msg) self.h.store() def untrain_spam(self, msg): self.open('c') self.h.untrain_spam(msg) self.h.store() def main(profiling=False): h = HammieFilter() actions = [] opts, args = getopt.getopt(sys.argv[1:], 'hvxd:p:nfgstGSo:P', ['help', 'version', 'examples', 'option=']) create_newdb = False do_profile = False for opt, arg in opts: if opt in ('-h', '--help'): usage(0) elif opt in ('-v', '--version'): version() elif opt in ('-x', '--examples'): examples() elif opt in ('-o', '--option'): Options.options.set_from_cmdline(arg, sys.stderr) elif opt == '-f': actions.append(h.filter) elif opt == '-g': actions.append(h.train_ham) elif opt == '-s': actions.append(h.train_spam) elif opt == '-t': actions.append(h.filter_train) elif opt == '-G': actions.append(h.untrain_ham) elif opt == '-S': actions.append(h.untrain_spam) elif opt == '-P': do_profile = True if not profiling: try: import cProfile except ImportError: pass else: return cProfile.run("main(True)") elif opt == "-n": create_newdb = True h.dbname, h.usedb = storage.database_type(opts) if create_newdb or not os.path.exists(h.dbname): h.newdb() print >> sys.stderr, "Created new database in", h.dbname if create_newdb: sys.exit(0) if actions == []: actions = [h.filter] if not args: args = ["-"] for fname in args: mbox = mboxutils.getmbox(fname) for msg in mbox: for action in actions: action(msg) if args == ["-"]: unixfrom = msg.get_unixfrom() is not None else: unixfrom = True result = mboxutils.as_string(msg, unixfrom=unixfrom) sys.stdout.write(result) if __name__ == "__main__": main() spambayes-1.1a6/scripts/sb_imapfilter.py0000664000076500000240000016026711143321335020552 0ustar skipstaff00000000000000#!/usr/bin/env python """An IMAP filter. An IMAP message box is scanned and all non-scored messages are scored and (where necessary) filtered. Usage: sb_imapfilter [options] note: option values with spaces in them must be enclosed in double quotes options: -p dbname : pickled training database filename -d dbname : dbm training database filename -t : train contents of spam folder and ham folder -c : classify inbox -h : display this message -v : verbose mode -P : security option to prompt for imap password, rather than look in options["imap", "password"] -e y/n : expunge/purge messages on exit (y) or not (n) -i debuglvl : a somewhat mysterious imaplib debugging level (4 is a good level, and suitable for bug reports) -l minutes : period of time between filtering operations -b : Launch a web browser showing the user interface. -o section:option:value : set [section, option] in the options database to value Examples: Classify inbox, with dbm database sb_imapfilter -c -d bayes.db Train Spam and Ham, then classify inbox, with dbm database sb_imapfilter -t -c -d bayes.db Train Spam and Ham only, with pickled database sb_imapfilter -t -p bayes.db Warnings: o We never delete mail, unless you use the -e/purge option, but we do mark a lot as deleted, and your mail client might remove that for you. We try to only mark as deleted once the moved/altered message is correctly saved, but things might go wrong. We *strongly* recommend that you try this script out on mail that you can recover from somewhere else, at least at first. """ from __future__ import generators todo = """ o IMAP supports authentication via other methods than the plain-text password method that we are using at the moment. Neither of the servers I have access to offer any alternative method, however. If someone's does, then it would be nice to offer this. Thanks to #1169939 we now support CRAM_MD5 if available. It'd still be good to support others, though. o Usernames should be able to be literals as well as quoted strings. This might help if the username/password has special characters like accented characters. o Suggestions? """ # This module is part of the SpamBayes project, which is Copyright 2002-2007 # The Python Software Foundation and is covered by the Python Software # Foundation license. __author__ = "Tony Meyer , Tim Stone" __credits__ = "All the SpamBayes folk. The original filter design owed " \ "much to isbg by Roger Binns (http://www.rogerbinns.com/isbg)." # If we are running as a frozen application, then chances are that # output is just lost. We'd rather log this, like sb_server and Oulook # log, so that the user can pull up the output if possible. We could just # rely on the user piping the output appropriately, but would rather have # more control. The sb_server tray application only does this if not # running in a console window, but we do it whenever we are frozen. import os import sys if hasattr(sys, "frozen"): # We want to move to logging module later, so for now, we # hack together a simple logging strategy. try: import win32api except ImportError: if sys.platform == "win32": # Fall back to CWD, but warn user. status = "Warning: your log is stored in the current " \ "working directory. We recommend installing " \ "the pywin32 extensions, so that the log is " \ "stored in the Windows temp directory." temp_dir = os.getcwd() else: # Try for a /tmp directory. if os.path.isdir("/tmp"): temp_dir = "/tmp" status = "Log file opened in /tmp" else: status = "Warning: your log is stored in the current " \ "working directory. If this does not suit you " \ "please let the spambayes@python.org crowd know " \ "so that an alternative can be arranged." else: temp_dir = win32api.GetTempPath() status = "Log file opened in " + temp_dir for i in range(3, 0, -1): try: os.unlink(os.path.join(temp_dir, "SpamBayesIMAP%d.log" % (i+1))) except os.error: pass try: os.rename( os.path.join(temp_dir, "SpamBayesIMAP%d.log" % i), os.path.join(temp_dir, "SpamBayesIMAP%d.log" % (i+1)) ) except os.error: pass # Open this log, as unbuffered, so crashes still get written. sys.stdout = open(os.path.join(temp_dir,"SpamBayesIMAP1.log"), "wt", 0) sys.stderr = sys.stdout import socket import re import time import getopt import types import thread import email import email.Parser from getpass import getpass from email.Utils import parsedate from spambayes import Stats from spambayes import message from spambayes.Options import options, optionsPathname from spambayes import storage, Dibbler from spambayes.UserInterface import UserInterfaceServer from spambayes.ImapUI import IMAPUserInterface, LoginFailure from spambayes.Version import get_current_version from imaplib import IMAP4 from imaplib import Time2Internaldate try: if options["imap", "use_ssl"]: from imaplib import IMAP4_SSL as BaseIMAP else: from imaplib import IMAP4 as BaseIMAP except ImportError: from imaplib import IMAP4 as BaseIMAP class BadIMAPResponseError(Exception): """An IMAP command returned a non-"OK" response.""" def __init__(self, command, response): self.command = command self.response = response def __str__(self): return "The command '%s' failed to give an OK response.\n%s" % \ (self.command, self.response) class IMAPSession(BaseIMAP): '''A class extending the IMAP4 class, with a few optimizations''' timeout = 60 # seconds def __init__(self, server, debug=0, do_expunge = options["imap", "expunge"] ): if ":" in server: server, port = server.split(':', 1) port = int(port) else: if options["imap", "use_ssl"]: port = 993 else: port = 143 # There's a tricky situation where if use_ssl is False, but we # try to connect to a IMAP over SSL server, we will just hang # forever, waiting for a response that will never come. To # get past this, just for the welcome message, we install a # timeout on the connection. Normal service is then returned. # This only applies when we are not using SSL. if not hasattr(self, "ssl"): readline = self.readline self.readline = self.readline_timeout try: BaseIMAP.__init__(self, server, port) except (BaseIMAP.error, socket.gaierror, socket.error): if options["globals", "verbose"]: print >> sys.stderr, "Cannot connect to server", server, "on port", port if not hasattr(self, "ssl"): print >> sys.stderr, ("If you are connecting to an SSL server," "please ensure that you\n" "have the 'Use SSL' option enabled.") self.connected = False else: self.connected = True if not hasattr(self, "ssl"): self.readline = readline self.debug = debug self.do_expunge = do_expunge self.server = server self.port = port self.logged_in = False # For efficiency, we remember which folder we are currently # in, and only send a select command to the IMAP server if # we want to *change* folders. This functionality is used by # both IMAPMessage and IMAPFolder. self.current_folder = None # We override the base read so that we only read a certain amount # of data at a time. OS X and Python has problems with getting # large amounts of memory at a time, so maybe this will be a way we # can work around that (I don't know, and don't have a mac to test, # but we need to try something). self._read = self.read self.read = self.safe_read def readline_timeout(self): """Read line from remote, possibly timing out.""" st_time = time.time() self.sock.setblocking(False) buffer = [] while True: if (time.time() - st_time) > self.timeout: if options["globals", "verbose"]: print >> sys.stderr, "IMAP Timing out" break try: data = self.sock.recv(1) except socket.error, e: if e[0] == 10035: # Nothing to receive, keep going. continue raise if not data: break if data == '\n': break buffer.append(data) self.sock.setblocking(True) return "".join(buffer) def login(self, username, pwd): """Log in to the IMAP server, catching invalid username/password.""" assert self.connected, "Must be connected before logging in." if 'AUTH=CRAM-MD5' in self.capabilities: login_func = self.login_cram_md5 args = (username, pwd) description = "MD5" else: login_func = BaseIMAP.login # superclass login args = (self, username, pwd) description = "plain-text" try: login_func(*args) except BaseIMAP.error, e: msg = "The username (%s) and/or password (sent in %s) may " \ "be incorrect." % (username, description) raise LoginFailure(msg) self.logged_in = True def logout(self): """Log off from the IMAP server, possibly expunging. Note that most, if not all, of the expunging is probably done in SelectFolder, rather than here, for purposes of speed.""" # We may never have logged in, in which case we do nothing. if self.connected and self.logged_in and self.do_expunge: # Expunge messages from the ham, spam and unsure folders. for fol in ["spam_folder", "unsure_folder", "ham_folder"]: folder_name = options["imap", fol] if folder_name: self.select(folder_name) self.expunge() # Expunge messages from the ham and spam training folders. for fol_list in ["ham_train_folders", "spam_train_folders",]: for fol in options["imap", fol_list]: self.select(fol) self.expunge() BaseIMAP.logout(self) # superclass logout def check_response(self, command, IMAP_response): """A utility function to check the response from IMAP commands. Raises BadIMAPResponseError if the response is not OK. Returns the data segment of the response otherwise.""" response, data = IMAP_response if response != "OK": raise BadIMAPResponseError(command, IMAP_response) return data def SelectFolder(self, folder): """A method to point ensuing IMAP operations at a target folder. This is essentially a wrapper around the IMAP select command, which ignores the command if the folder is already selected.""" if self.current_folder != folder: if self.current_folder != None and self.do_expunge: # It is faster to do close() than a single # expunge when we log out (because expunge returns # a list of all the deleted messages which we don't do # anything with). self.close() self.current_folder = None if folder == "": # This is Python bug #845560 - if the empty string is # passed, we get a traceback, not just an 'invalid folder' # error, so raise our own error. raise BadIMAPResponseError("select", "Cannot have empty string as " "folder name in select") # We *always* use SELECT and not EXAMINE, because this # speeds things up considerably. response = self.select(folder, None) data = self.check_response("select %s" % (folder,), response) self.current_folder = folder return data number_re = re.compile(r"{\d+}") folder_re = re.compile(r"\(([\w\\ ]*)\) ") def folder_list(self): """Return a alphabetical list of all folders available on the server.""" response = self.list() try: all_folders = self.check_response("list", response) except BadIMAPResponseError: # We want to keep going, so just print out a warning, and # return an empty list. if options["globals", "verbose"]: print >> sys.stderr, "Could not retrieve folder list." return [] folders = [] for fol in all_folders: # Sigh. Some servers may give us back the folder name as a # literal, so we need to crunch this out. if isinstance(fol, types.TupleType): m = self.number_re.search(fol[0]) if not m: # Something is wrong here! Skip this folder. continue fol = '%s"%s"' % (fol[0][:m.start()], fol[1]) m = self.folder_re.search(fol) if not m: # Something is not good with this folder, so skip it. continue name_attributes = fol[:m.end()-1] # IMAP is a truly odd protocol. The delimiter is # only the delimiter for this particular folder - each # folder *may* have a different delimiter self.folder_delimiter = fol[m.end()+1:m.end()+2] # A bit of a hack, but we really need to know if this is # the case. if self.folder_delimiter == ',': print >> sys.stderr, ("WARNING: Your imap server uses a comma as the " "folder delimiter. This may cause unpredictable " \ "errors.") folders.append(fol[m.end()+4:].strip('"')) folders.sort() return folders # A flag can have any character in the ascii range 32-126 except for # (){ %*"\ FLAG_CHARS = "" for i in range(32, 127): if not chr(i) in ['(', ')', '{', ' ', '%', '*', '"', '\\']: FLAG_CHARS += chr(i) FLAG = r"\\?[" + re.escape(FLAG_CHARS) + r"]+" # The empty flag set "()" doesn't match, so that extract_fetch_data() # returns data["FLAGS"] == None FLAGS_RE = re.compile(r"(FLAGS) (\((" + FLAG + r" )*(" + FLAG + r")\))") INTERNALDATE_RE = re.compile(r"(INTERNALDATE) (\"\d{1,2}\-[A-Za-z]{3,3}\-" + r"\d{2,4} \d{2,2}\:\d{2,2}\:\d{2,2} " + r"[\+\-]\d{4,4}\")") RFC822_RE = re.compile(r"(RFC822) (\{[\d]+\})") BODY_PEEK_RE = re.compile(r"(BODY\[\]) (\{[\d]+\})") RFC822_HEADER_RE = re.compile(r"(RFC822.HEADER) (\{[\d]+\})") UID_RE = re.compile(r"(UID) ([\d]+)") UID_RE2 = re.compile(r" *(UID) ([\d]+)\)") FETCH_RESPONSE_RE = re.compile(r"([0-9]+) \(([" + \ re.escape(FLAG_CHARS) + r"\"\{\}\(\)\\ ]*)\)?") LITERAL_RE = re.compile(r"^\{[\d]+\}$") def _extract_fetch_data(self, response): """This does the real work of extracting the data, for each message number. """ # We support the following FETCH items: # FLAGS # INTERNALDATE # RFC822 # UID # RFC822.HEADER # BODY.PEEK # All others are ignored. if isinstance(response, types.StringTypes): response = (response,) data = {} expected_literal = None if self.UID_RE2.match(response[-1]): response = response[:-1] for part in response: # We ignore parentheses by themselves, for convenience. if part == ')': continue if expected_literal: # This should be a literal of a certain size. key, expected_size = expected_literal ## if len(part) != expected_size: ## raise BadIMAPResponseError(\ ## "FETCH response (wrong size literal %d != %d)" % \ ## (len(part), expected_size), response) data[key] = part expected_literal = None continue # The first item will always be the message number. mo = self.FETCH_RESPONSE_RE.match(part) if mo: data["message_number"] = mo.group(1) rest = mo.group(2) else: raise BadIMAPResponseError("FETCH response", response) for r in [self.FLAGS_RE, self.INTERNALDATE_RE, self.RFC822_RE, self.UID_RE, self.RFC822_HEADER_RE, self.BODY_PEEK_RE]: mo = r.search(rest) if mo is not None: if self.LITERAL_RE.match(mo.group(2)): # The next element will be a literal. expected_literal = (mo.group(1), int(mo.group(2)[1:-1])) else: data[mo.group(1)] = mo.group(2) return data def extract_fetch_data(self, response): """Extract data from the response given to an IMAP FETCH command. The data is put into a dictionary, which is returned, where the keys are the fetch items. """ # There may be more than one message number in the response, so # handle separately. if isinstance(response, types.StringTypes): response = (response,) data = {} for msg in response: msg_data = self._extract_fetch_data(msg) if msg_data: # Maybe there are two about the same message number! num = msg_data["message_number"] if num in data: data[num].update(msg_data) else: data[num] = msg_data return data # Maximum amount of data that will be read at any one time. MAXIMUM_SAFE_READ = 4096 def safe_read(self, size): """Read data from remote, but in manageable sizes.""" data = [] while size > 0: if size < self.MAXIMUM_SAFE_READ: to_collect = size else: to_collect = self.MAXIMUM_SAFE_READ data.append(self._read(to_collect)) size -= self.MAXIMUM_SAFE_READ return "".join(data) class IMAPMessage(message.SBHeaderMessage): def __init__(self): message.SBHeaderMessage.__init__(self) self.folder = None self.previous_folder = None self.rfc822_command = "(BODY.PEEK[])" self.rfc822_key = "BODY[]" self.got_substance = False self.invalid = False self.could_not_retrieve = False self.imap_server = None def extractTime(self): """When we create a new copy of a message, we need to specify a timestamp for the message, if we can't get the information from the IMAP server itself. If the message has a valid date header we use that. Otherwise, we use the current time.""" message_date = self["Date"] if message_date is not None: parsed_date = parsedate(message_date) if parsed_date is not None: try: return Time2Internaldate(time.mktime(parsed_date)) except ValueError: # Invalid dates can cause mktime() to raise a # ValueError, for example: # >>> time.mktime(parsedate("Mon, 06 May 0102 10:51:16 -0100")) # Traceback (most recent call last): # File "", line 1, in ? # ValueError: year out of range # (Why this person is getting mail from almost two # thousand years ago is another question ). # In any case, we just pass and use the current date. pass except OverflowError: pass return Time2Internaldate(time.time()) def get_full_message(self): """Retrieve the RFC822 message from the IMAP server and return a new IMAPMessage object that has the same details as this message, but also has the substance.""" if self.got_substance: return self assert self.id, "Cannot get substance of message without an id" assert self.uid, "Cannot get substance of message without an UID" assert self.imap_server, "Cannot do anything without IMAP connection" # First, try to select the folder that the message is in. try: self.imap_server.SelectFolder(self.folder.name) except BadIMAPResponseError: # Can't select the folder, so getting the substance will not # work. self.could_not_retrieve = True print >> sys.stderr, "Could not select folder %s for message " \ "%s (uid %s)" % (self.folder.name, self.id, self.uid) return self # Now try to fetch the substance of the message. try: response = self.imap_server.uid("FETCH", self.uid, self.rfc822_command) except MemoryError: # Really big messages can trigger a MemoryError here. # The problem seems to be line 311 (Python 2.3) of socket.py, # which has "return "".join(buffers)". This has also caused # problems with Mac OS X 10.3, which apparently is very stingy # with memory (the malloc calls fail!). The problem then is # line 301 of socket.py which does # "data = self._sock.recv(recv_size)". # We want to handle this gracefully, although we can't really # do what we do later, and rewrite the message, since we can't # load it in the first place. Maybe an elegant solution would # be to get the message in parts, or just use the first X # characters for classification. For now, we just carry on, # warning the user and ignoring the message. self.could_not_retrieve = True print >> sys.stderr, "MemoryError with message %s (uid %s)" % \ (self.id, self.uid) return self command = "uid fetch %s" % (self.uid,) response_data = self.imap_server.check_response(command, response) data = self.imap_server.extract_fetch_data(response_data) # The data will be a dictionary - hopefully with only one element, # but maybe more than one. The key is the message number, which we # do not have (we use the UID instead). So we look through the # message and use the first data of the right type we find. rfc822_data = None for msg_data in data.itervalues(): if self.rfc822_key in msg_data: rfc822_data = msg_data[self.rfc822_key] break if rfc822_data is None: raise BadIMAPResponseError("FETCH response", response_data) try: new_msg = email.message_from_string(rfc822_data, IMAPMessage) # We use a general 'except' because the email package doesn't # always return email.Errors (it can return a TypeError, for # example) if the email is invalid. In any case, we want # to keep going, and not crash, because we might leave the # user's mailbox in a bad state if we do. Better to soldier on. except: # Yikes! Barry set this to return at this point, which # would work ok for training (IIRC, that's all he's # using it for), but for filtering, what happens is that # the message ends up blank, but ok, so the original is # flagged to be deleted, and a new (almost certainly # unsure) message, *with only the spambayes headers* is # created. The nice solution is still to do what sb_server # does and have a X-Spambayes-Exception header with the # exception data and then the original message. self.invalid = True text, details = message.insert_exception_header( rfc822_data, self.id) self.invalid_content = text self.got_substance = True # Print the exception and a traceback. print >> sys.stderr, details return self new_msg.folder = self.folder new_msg.previous_folder = self.previous_folder new_msg.rfc822_command = self.rfc822_command new_msg.rfc822_key = self.rfc822_key new_msg.imap_server = self.imap_server new_msg.uid = self.uid new_msg.setId(self.id) new_msg.got_substance = True if not new_msg.has_key(options["Headers", "mailid_header_name"]): new_msg[options["Headers", "mailid_header_name"]] = self.id if options["globals", "verbose"]: sys.stdout.write(chr(8) + "*") return new_msg def MoveTo(self, dest): '''Note that message should move to another folder. No move is carried out until Save() is called, for efficiency.''' if self.previous_folder is None: self.previous_folder = self.folder self.folder = dest def as_string(self, unixfrom=False): # Basically the same as the parent class's except that we handle # the case where the data was unparsable, so we haven't done any # filtering, and we are not actually a proper email.Message object. # We also don't mangle the from line; the server must take care of # this. if self.invalid: return self._force_CRLF(self.invalid_content) else: return message.SBHeaderMessage.as_string(self, unixfrom, mangle_from_=False) recent_re = re.compile(r"\\Recent ?| ?\\Recent") def Save(self): """Save message to IMAP server. We can't actually update the message with IMAP, so what we do is create a new message and delete the old one.""" assert self.folder is not None, \ "Can't save a message that doesn't have a folder." assert self.id, "Can't save a message that doesn't have an id." assert self.imap_server, "Can't do anything without IMAP connection." response = self.imap_server.uid("FETCH", self.uid, "(FLAGS INTERNALDATE)") command = "fetch %s (flags internaldate)" % (self.uid,) response_data = self.imap_server.check_response(command, response) data = self.imap_server.extract_fetch_data(response_data) # The data will be a dictionary - hopefully with only one element, # but maybe more than one. The key is the message number, which we # do not have (we use the UID instead). So we look through the # message and use the last data of the right type we find. msg_time = self.extractTime() flags = None for msg_data in data.itervalues(): if "INTERNALDATE" in msg_data: msg_time = msg_data["INTERNALDATE"] if "FLAGS" in msg_data: flags = msg_data["FLAGS"] # The \Recent flag can be fetched, but cannot be stored # We must remove it from the list if it is there. flags = self.recent_re.sub("", flags) # We try to save with flags and time, then with just the # time, then with the flags and the current time, then with just # the current time. The first should work, but the first three # sometimes (due to the quirky IMAP server) fail. for flgs, tme in [(flags, msg_time), (None, msg_time), (flags, Time2Internaldate(time.time())), (None, Time2Internaldate(time.time()))]: try: response = self.imap_server.append(self.folder.name, flgs, tme, self.as_string()) except BaseIMAP.error: continue try: self.imap_server.check_response("", response) except BadIMAPResponseError: pass else: break else: command = "append %s %s %s %s" % (self.folder.name, flgs, tme, self.as_string) raise BadIMAPResponseError(command) if self.previous_folder is None: self.imap_server.SelectFolder(self.folder.name) else: self.imap_server.SelectFolder(self.previous_folder.name) self.previous_folder = None response = self.imap_server.uid("STORE", self.uid, "+FLAGS.SILENT", "(\\Deleted \\Seen)") command = "set %s to be deleted and seen" % (self.uid,) self.imap_server.check_response(command, response) # Not all IMAP servers immediately offer the new message, but # we need to find it to get the new UID. We need to wait until # the server offers up an EXISTS command, so we no-op until that # is the case. # See [ 941596 ] sb_imapfilter.py not adding headers / moving messages # We use the recent() function, which no-ops if necessary. We try # 100 times, and then give up. If a message arrives independantly, # and we are told about it before our message, then this could # cause trouble, but that would be one weird server. for i in xrange(100): response = self.imap_server.recent() data = self.imap_server.check_response("recent", response) if data[0] is not None: if options["globals", "verbose"]: print >> sys.stderr, "[imapfilter] found saved message", self.uid, print >> sys.stderr, "in iteration", i break else: if options["globals", "verbose"]: print >> sys.stderr, ("[imapfilter] can't find saved message after" "100 iterations:"), self.uid # raise BadIMAPResponseError("recent", "Cannot find saved message") # We need to update the UID, as it will have changed. # Although we don't use the UID to keep track of messages, we do # have to use it for IMAP operations. self.imap_server.SelectFolder(self.folder.name) search_string = "(UNDELETED HEADER %s \"%s\")" % \ (options["Headers", "mailid_header_name"], self.id.replace('\\',r'\\').replace('"',r'\"')) response = self.imap_server.uid("SEARCH", search_string) data = self.imap_server.check_response("search " + search_string, response) new_id = data[0] # See [ 870799 ] imap trying to fetch invalid message UID # It seems that although the save gave a "NO" response to the # first save, the message was still saved (without the flags, # probably). This really isn't good behaviour on the server's # part, but, as usual, we try and deal with it. So, if we get # more than one undeleted message with the same SpamBayes id, # delete all of them apart from the last one, and use that. multiple_ids = new_id.split() for id_to_remove in multiple_ids[:-1]: response = self.imap_server.uid("STORE", id_to_remove, "+FLAGS.SILENT", "(\\Deleted \\Seen)") command = "silently delete and make seen %s" % (id_to_remove,) self.imap_server.check_response(command, response) if multiple_ids: new_id = multiple_ids[-1] else: # Let's hope it doesn't, but, just in case, if the search # turns up empty, we make the assumption that the new message # is the last one with a recent flag. response = self.imap_server.uid("SEARCH", "RECENT") data = self.imap_server.check_response("search recent", response) new_id = data[0] if new_id.find(' ') > -1: ids = new_id.split(' ') new_id = ids[-1] # Ok, now we're in trouble if we still haven't found it. # We make a huge assumption that the new message is the one # with the highest UID (they are sequential, so this will be # ok as long as another message hasn't also arrived). if new_id == "": response = self.imap_server.uid("SEARCH", "ALL") data = self.imap_server.check_response("search all", response) new_id = data[0] if new_id.find(' ') > -1: ids = new_id.split(' ') new_id = ids[-1] self.uid = new_id class IMAPFolder(object): def __init__(self, folder_name, imap_server, stats): self.name = folder_name self.imap_server = imap_server self.stats = stats # Unique names for cached messages - see _generate_id below. self.lastBaseMessageName = '' self.uniquifier = 2 def __cmp__(self, obj): """Two folders are equal if their names are equal.""" if obj is None: return False return cmp(self.name, obj.name) def __iter__(self): """Iterate through the messages in this IMAP folder.""" for key in self.keys(): yield self[key] def keys(self): '''Returns *uids* for all the messages in the folder not marked as deleted.''' self.imap_server.SelectFolder(self.name) response = self.imap_server.uid("SEARCH", "UNDELETED") data = self.imap_server.check_response("search undeleted", response) if data[0]: return data[0].split(' ') else: return [] custom_header_id_re = re.compile(re.escape(\ options["Headers", "mailid_header_name"]) + "\:\s*(\d+(?:\-\d)?)", re.IGNORECASE) message_id_re = re.compile("Message-ID\: ?\<([^\n\>]+)\>", re.IGNORECASE) def __getitem__(self, key): """Return message matching the given *uid*. The messages returned have no substance (so this should be reasonably quick, even with large messages). You need to call get_full_message() on the returned message to get the substance of the message from the server.""" self.imap_server.SelectFolder(self.name) # Using RFC822.HEADER.LINES would be better here, but it seems # that not all servers accept it, even though it is in the RFC response = self.imap_server.uid("FETCH", key, "RFC822.HEADER") response_data = self.imap_server.check_response(\ "fetch %s rfc822.header" % (key,), response) data = self.imap_server.extract_fetch_data(response_data) # The data will be a dictionary - hopefully with only one element, # but maybe more than one. The key is the message number, which we # do not have (we use the UID instead). So we look through the # message and use the first data of the right type we find. headers = None for msg_data in data.itervalues(): if "RFC822.HEADER" in msg_data: headers = msg_data["RFC822.HEADER"] break if headers is None: raise BadIMAPResponseError("FETCH response", response_data) # Create a new IMAPMessage object, which will be the return value. msg = IMAPMessage() msg.folder = self msg.uid = key msg.imap_server = self.imap_server # We use the MessageID header as the ID for the message, as long # as it is available, and if not, we add our own. # Search for our custom id first, for backwards compatibility. for id_header_re in [self.custom_header_id_re, self.message_id_re]: mo = id_header_re.search(headers) if mo: msg.setId(mo.group(1)) break else: newid = self._generate_id() if options["globals", "verbose"]: print >> sys.stderr, "[imapfilter] saving", msg.uid, "with new id:", newid msg.setId(newid) # Unfortunately, we now have to re-save this message, so that # our id is stored on the IMAP server. The vast majority of # messages have Message-ID headers, from what I can tell, so # we should only rarely have to do this. It's less often than # with the previous solution, anyway! # msg = msg.get_full_message() # msg.Save() if options["globals", "verbose"]: sys.stdout.write(".") return msg # Lifted straight from sb_server.py (under the name getNewMessageName) def _generate_id(self): # The message id is the time it arrived, with a uniquifier # appended if two arrive within one clock tick of each other. messageName = "%10.10d" % long(time.time()) if messageName == self.lastBaseMessageName: messageName = "%s-%d" % (messageName, self.uniquifier) self.uniquifier += 1 else: self.lastBaseMessageName = messageName self.uniquifier = 2 return messageName def Train(self, classifier, isSpam): """Train folder as spam/ham.""" num_trained = 0 for msg in self: if msg.GetTrained() == (not isSpam): msg = msg.get_full_message() if msg.could_not_retrieve: # Something went wrong, and we couldn't even get # an invalid message, so just skip this one. # Annoyingly, we'll try to do it every time the # script runs, but hopefully the user will notice # the errors and move it soon enough. continue msg.delSBHeaders() classifier.unlearn(msg.tokenize(), not isSpam) if isSpam: old_class = options["Headers", "header_ham_string"] else: old_class = options["Headers", "header_spam_string"] # Once the message has been untrained, it's training memory # should reflect that on the off chance that for some # reason the training breaks. msg.RememberTrained(None) else: old_class = None if msg.GetTrained() is None: msg = msg.get_full_message() if msg.could_not_retrieve: continue saved_headers = msg.currentSBHeaders() msg.delSBHeaders() classifier.learn(msg.tokenize(), isSpam) num_trained += 1 msg.RememberTrained(isSpam) self.stats.RecordTraining(not isSpam, old_class=old_class) if isSpam: move_opt_name = "move_trained_spam_to_folder" else: move_opt_name = "move_trained_ham_to_folder" if options["imap", move_opt_name] != "": # We need to restore the SpamBayes headers. for header, value in saved_headers.items(): msg[header] = value msg.MoveTo(IMAPFolder(options["imap", move_opt_name], self.imap_server, self.stats)) msg.Save() return num_trained def Filter(self, classifier, spamfolder, unsurefolder, hamfolder): count = {} count["ham"] = 0 count["spam"] = 0 count["unsure"] = 0 for msg in self: cls = msg.GetClassification() if cls is None or hamfolder is not None: if options["globals", "verbose"]: print >> sys.stderr, "[imapfilter] classified as %s:" % cls, msg.uid msg = msg.get_full_message() if msg.could_not_retrieve: # Something went wrong, and we couldn't even get # an invalid message, so just skip this one. # Annoyingly, we'll try to do it every time the # script runs, but hopefully the user will notice # the errors and move it soon enough. if options["globals", "verbose"]: print >> sys.stderr, "[imapfilter] could not retrieve:", msg.uid continue (prob, clues) = classifier.spamprob(msg.tokenize(), evidence=True) # Add headers and remember classification. msg.delSBHeaders() msg.addSBHeaders(prob, clues) self.stats.RecordClassification(prob) cls = msg.GetClassification() if cls == options["Headers", "header_ham_string"]: if hamfolder: if options["globals", "verbose"]: print >> sys.stderr, "[imapfilter] moving to ham folder:", print >> sys.stderr, msg.uid msg.MoveTo(hamfolder) # Otherwise, we leave ham alone. count["ham"] += 1 elif cls == options["Headers", "header_spam_string"]: if options["globals", "verbose"]: print >> sys.stderr, "[imapfilter] moving to spam folder:", print >> sys.stderr, msg.uid msg.MoveTo(spamfolder) count["spam"] += 1 else: if options["globals", "verbose"]: print >> sys.stderr, "[imapfilter] moving to unsure folder:", msg.uid msg.MoveTo(unsurefolder) count["unsure"] += 1 msg.Save() else: if options["globals", "verbose"]: print >> sys.stderr, "[imapfilter] already classified:", msg.uid return count class IMAPFilter(object): def __init__(self, classifier, stats): self.spam_folder = None self.unsure_folder = None self.ham_folder = None self.classifier = classifier self.imap_server = None self.stats = stats def Train(self): assert self.imap_server, "Cannot do anything without IMAP server." if options["globals", "verbose"]: t = time.time() total_trained = 0 for is_spam, option_name in [(False, "ham_train_folders"), (True, "spam_train_folders")]: training_folders = options["imap", option_name] for fol in training_folders: # Select the folder to make sure it exists try: self.imap_server.SelectFolder(fol) except BadIMAPResponseError: print >> sys.stderr, "Skipping", fol, "as it cannot be selected." continue if options['globals', 'verbose']: print >> sys.stderr, (" Training %s folder %s" % (["ham", "spam"][is_spam], fol)) folder = IMAPFolder(fol, self.imap_server, self.stats) num_trained = folder.Train(self.classifier, is_spam) total_trained += num_trained if options['globals', 'verbose']: print >> sys.stderr, "\n ", num_trained, "trained." if total_trained: self.classifier.store() if options["globals", "verbose"]: print >> sys.stderr, ("Training took %.4f seconds, %s messages were trained." % (time.time() - t, total_trained)) def Filter(self): assert self.imap_server, "Cannot do anything without IMAP server." if not self.spam_folder: spam_folder_name = options["imap", "spam_folder"] if options["globals", "verbose"]: print >> sys.stderr, "[imapfilter] spam folder:", spam_folder_name self.spam_folder = IMAPFolder( spam_folder_name, self.imap_server, self.stats) if not self.unsure_folder: unsure_folder_name = options["imap", "unsure_folder"] if options["globals", "verbose"]: print >> sys.stderr, "[imapfilter] unsure folder:", unsure_folder_name self.unsure_folder = IMAPFolder( unsure_folder_name, self.imap_server, self.stats) ham_folder_name = options["imap", "ham_folder"] if options["globals", "verbose"]: print >> sys.stderr, "[imapfilter] ham folder:", ham_folder_name if ham_folder_name and not self.ham_folder: self.ham_folder = IMAPFolder(ham_folder_name, self.imap_server, self.stats) if options["globals", "verbose"]: t = time.time() count = {} count["ham"] = 0 count["spam"] = 0 count["unsure"] = 0 # Select the ham, spam and unsure folders to make sure they exist. try: self.imap_server.SelectFolder(self.spam_folder.name) except BadIMAPResponseError: print >> sys.stderr, "Cannot select spam folder. Please check configuration." sys.exit(-1) try: self.imap_server.SelectFolder(self.unsure_folder.name) except BadIMAPResponseError: print >> sys.stderr, "Cannot select unsure folder. Please check configuration." sys.exit(-1) if self.ham_folder: try: self.imap_server.SelectFolder(self.ham_folder.name) except BadIMAPResponseError: print >> sys.stderr, "Cannot select ham folder. Please check configuration." sys.exit(-1) for filter_folder in options["imap", "filter_folders"]: # Select the folder to make sure it exists. try: self.imap_server.SelectFolder(filter_folder) except BadIMAPResponseError: print >> sys.stderr, "Cannot select", filter_folder, "... skipping." continue folder = IMAPFolder(filter_folder, self.imap_server, self.stats) subcount = folder.Filter(self.classifier, self.spam_folder, self.unsure_folder, self.ham_folder) for key in count.keys(): count[key] += subcount.get(key, 0) if options["globals", "verbose"]: if count is not None: print >> sys.stderr, ("\nClassified %s ham, %s spam, and %s unsure." % (count["ham"], count["spam"], count["unsure"])) print >> sys.stderr, "Classifying took %.4f seconds." % (time.time() - t,) def servers(promptForPass = False): """Returns a list containing a tuple (server,user,passwd) for each IMAP server in options. If promptForPass is True or at least on password is missing from options, prompts the user for each server's password. """ servers = options["imap", "server"] usernames = options["imap", "username"] pwds = options["imap", "password"] if promptForPass or len(pwds) < len(usernames): pwds = [] for u in usernames: pwds.append(getpass("Enter password for %s:" % (u,))) return zip(servers, usernames, pwds) def run(force_UI=False): try: opts, args = getopt.getopt(sys.argv[1:], 'hbPtcvl:e:i:d:p:o:', ["verbose"]) except getopt.error, msg: print >> sys.stderr, str(msg) + '\n\n' + __doc__ sys.exit() doTrain = False doClassify = False doExpunge = options["imap", "expunge"] imapDebug = 0 sleepTime = 0 promptForPass = False launchUI = False for opt, arg in opts: if opt == '-h': print >> sys.stderr, __doc__ sys.exit() elif opt == "-b": launchUI = True elif opt == '-t': doTrain = True elif opt == '-P': promptForPass = True elif opt == '-c': doClassify = True elif opt in ('-v', '--verbose'): options["globals", "verbose"] = True elif opt == '-e': if arg == 'y': doExpunge = True else: doExpunge = False elif opt == '-i': imapDebug = int(arg) elif opt == '-l': sleepTime = int(arg) * 60 elif opt == '-o': options.set_from_cmdline(arg, sys.stderr) bdbname, useDBM = storage.database_type(opts) # Let the user know what they are using... v = get_current_version(); print "%s.\n" % (v.get_long_version("SpamBayes IMAP Filter"),) if options["globals", "verbose"]: print "Loading database %s..." % (bdbname), classifier = storage.open_storage(bdbname, useDBM) message_db = message.Message().message_info_db if options["globals", "verbose"]: print "Done." if not ( launchUI or force_UI or options["imap", "server"] ): print "You need to specify both a server and a username." sys.exit() servers_data = servers(promptForPass) # Load stats manager. stats = Stats.Stats(options, message_db) imap_filter = IMAPFilter(classifier, stats) # Web interface. We have changed the rules about this many times. # With 1.0.x, the rule is that the interface is served if we are # not classifying or training. However, this runs into the problem # that if we run with -l, we might still want to edit the options, # and we don't want to start a separate instance, because then the # database is accessed from two processes. # With 1.1.x, the rule is that the interface is also served if the # -l option is used, which means it is only not served if we are # doing a one-off classification/train. In that case, there would # probably not be enough time to get to the interface and interact # with it (and we don't want it to die halfway through!), and we # don't want to slow classification/training down, either. if sleepTime or not (doClassify or doTrain): imaps = [] for server, username, password in servers_data: if server == "": imaps.append(None) else: imaps.append(IMAPSession(server, imapDebug, doExpunge)) def close_db(): message_db.store() message_db.close() message.Message().message_info_db.store() message.Message().message_info_db.close() message.Message.message_info_db = None classifier.store() classifier.close() def change_db(): classifier = storage.open_storage(*storage.database_type(opts)) message.Message.message_info_db = message_db imap_filter = IMAPFilter(classifier, message_db) httpServer = UserInterfaceServer(options["html_ui", "port"]) pwds = [ x[2] for x in servers_data ] httpServer.register(IMAPUserInterface(classifier, imaps, pwds, IMAPSession, stats=stats, close_db=close_db, change_db=change_db)) launchBrowser = launchUI or options["html_ui", "launch_browser"] if sleepTime: # Run in a separate thread, as we have more work to do. thread.start_new_thread(Dibbler.run, (), {"launchBrowser":launchBrowser}) else: Dibbler.run(launchBrowser=launchBrowser) if doClassify or doTrain: imaps = [] for server, username, password in servers_data: imaps.append(((server, imapDebug, doExpunge), username, password)) # In order to make working with multiple servers easier, we # allow the user to have separate configuration files for each # server. These may specify different folders to watch, different # spam/unsure folders, or any other options (e.g. thresholds). # For each server we use the default (global) options, and load # the specific options on top. To facilitate this, we use a # restore point for the options with just the default (global) # options. # XXX What about when we are running with -l and change options # XXX via the web interface? We need to handle that, really. options.set_restore_point() while True: for (server, imapDebug, doExpunge), username, password in imaps: imap = IMAPSession(server, imapDebug, doExpunge) if options["globals", "verbose"]: print "Account: %s:%s" % (imap.server, imap.port) if imap.connected: # As above, we load a separate configuration file # for each server, if it exists. We look for a # file in the optionsPathname directory, with the # name server.name.ini or .spambayes_server_name_rc # XXX While 1.1 is in alpha these names can be # XXX changed if desired. Please let Tony know! basedir = os.path.dirname(optionsPathname) fn1 = os.path.join(basedir, imap.server + ".ini") fn2 = os.path.join(basedir, imap.server.replace(".", "_") + \ "_rc") for fn in (fn1, fn2): if os.path.exists(fn): options.merge_file(fn) try: imap.login(username, password) except LoginFailure, e: print str(e) continue imap_filter.imap_server = imap if doTrain: if options["globals", "verbose"]: print "Training" imap_filter.Train() if doClassify: if options["globals", "verbose"]: print "Classifying" imap_filter.Filter() imap.logout() options.revert_to_restore_point() else: # Failed to connect. This may be a temporary problem, # so just continue on and try again. If we are only # running once we will end, otherwise we'll try again # in sleepTime seconds. # XXX Maybe we should log this error message? pass if sleepTime: time.sleep(sleepTime) else: break if __name__ == '__main__': run() spambayes-1.1a6/scripts/sb_mailsort.py0000664000076500000240000001165711116563057020257 0ustar skipstaff00000000000000#! /usr/bin/env python """\ To train: %(program)s -t ham.mbox spam.mbox To filter mail (using .forward or .qmail): |%(program)s Maildir/ Mail/Spam/ To print the score and top evidence for a message or messages: %(program)s -s message [message ...] """ SPAM_CUTOFF = 0.57 SIZE_LIMIT = 5000000 # messages larger are not analyzed BLOCK_SIZE = 10000 RC_DIR = "~/.spambayes" DB_FILE = RC_DIR + "/wordprobs.cdb" CONFIG_FILE = RC_DIR + "/bayescustomize.ini" import sys import os import getopt import email import time import signal import socket import errno DB_FILE = os.path.expanduser(DB_FILE) def import_spambayes(): global mboxutils, CdbClassifier, tokenize if not os.environ.has_key('BAYESCUSTOMIZE'): os.environ['BAYESCUSTOMIZE'] = os.path.expanduser(CONFIG_FILE) from spambayes import mboxutils from spambayes.cdb_classifier import CdbClassifier from spambayes.tokenizer import tokenize program = sys.argv[0] # For usage(); referenced by docstring above def usage(code, msg=''): """Print usage message and sys.exit(code).""" if msg: print >> sys.stderr, msg print >> sys.stderr print >> sys.stderr, __doc__ % globals() sys.exit(code) def maketmp(dir): hostname = socket.gethostname() pid = os.getpid() fd = -1 for x in xrange(200): filename = "%d.%d.%s" % (time.time(), pid, hostname) pathname = "%s/tmp/%s" % (dir, filename) try: fd = os.open(pathname, os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0600) except IOError, exc: if exc[0] not in (errno.EINT, errno.EEXIST): raise else: break time.sleep(2) if fd == -1: raise SystemExit, "could not create a mail file" return (os.fdopen(fd, "wb"), pathname, filename) def train(bayes, msgs, is_spam): """Train bayes with all messages from a mailbox.""" mbox = mboxutils.getmbox(msgs) for msg in mbox: bayes.learn(tokenize(msg), is_spam) def train_messages(ham_name, spam_name): """Create database using messages.""" rc_dir = os.path.expanduser(RC_DIR) if not os.path.exists(rc_dir): print "Creating", RC_DIR, "directory..." os.mkdir(rc_dir) bayes = CdbClassifier() print 'Training with ham...' train(bayes, ham_name, False) print 'Training with spam...' train(bayes, spam_name, True) print 'Update probabilities and writing DB...' db = open(DB_FILE, "wb") bayes.save_wordinfo(db) db.close() print 'done' def filter_message(hamdir, spamdir): signal.signal(signal.SIGALRM, lambda s, f: sys.exit(1)) signal.alarm(24 * 60 * 60) # write message to temporary file (must be on same partition) tmpfile, pathname, filename = maketmp(hamdir) try: tmpfile.write(os.environ.get("DTLINE", "")) # delivered-to line bytes = 0 blocks = [] while 1: block = sys.stdin.read(BLOCK_SIZE) if not block: break bytes += len(block) if bytes < SIZE_LIMIT: blocks.append(block) tmpfile.write(block) tmpfile.close() if bytes < SIZE_LIMIT: msgdata = ''.join(blocks) del blocks msg = email.message_from_string(msgdata) del msgdata bayes = CdbClassifier(open(DB_FILE, 'rb')) prob = bayes.spamprob(tokenize(msg)) else: prob = 0.0 if prob > SPAM_CUTOFF: os.rename(pathname, "%s/new/%s" % (spamdir, filename)) else: os.rename(pathname, "%s/new/%s" % (hamdir, filename)) except: os.unlink(pathname) raise def print_message_score(msg_name, msg_fp): msg = email.message_from_file(msg_fp) bayes = CdbClassifier(open(DB_FILE, 'rb')) prob, evidence = bayes.spamprob(tokenize(msg), evidence=True) print msg_name, prob for word, prob in evidence: print ' ', repr(word), prob def main(): global DB_FILE, CONFIG_FILE try: opts, args = getopt.getopt(sys.argv[1:], 'tsd:c:') except getopt.error, msg: usage(2, msg) mode = 'sort' for opt, val in opts: if opt == '-t': mode = 'train' elif opt == '-s': mode = 'score' elif opt == '-d': DB_FILE = val elif opt == '-c': CONFIG_FILE = val else: assert 0, 'invalid option' import_spambayes() if mode == 'sort': if len(args) != 2: usage(2, 'wrong number of arguments') filter_message(args[0], args[1]) elif mode == 'train': if len(args) != 2: usage(2, 'wrong number of arguments') train_messages(args[0], args[1]) elif mode == 'score': if args: for msg in args: print_message_score(msg, open(msg)) else: print_message_score('', sys.stdin) if __name__ == "__main__": main() spambayes-1.1a6/scripts/sb_mboxtrain.py0000664000076500000240000002444511116563062020423 0ustar skipstaff00000000000000#! /usr/bin/env python ### Train spambayes on all previously-untrained messages in a mailbox. ### ### This keeps track of messages it's already trained by adding an ### X-Spambayes-Trained: header to each one. Then, if you move one to ### another folder, it will retrain that message. You would want to run ### this from a cron job on your server. """Usage: %(program)s [OPTIONS] ... Where OPTIONS is one or more of: -h show usage and exit -d DBNAME use the DBM store. A DBM file is larger than the pickle and creating it is slower, but loading it is much faster, especially for large word databases. Recommended for use with sb_filter or any procmail-based filter. -p DBNAME use the pickle store. A pickle is smaller and faster to create, but much slower to load. Recommended for use with sb_server and sb_xmlrpcserver. -g PATH mbox or directory of known good messages (non-spam) to train on. Can be specified more than once. -s PATH mbox or directory of known spam messages to train on. Can be specified more than once. -f force training, ignoring the trained header. Use this if you need to rebuild your database from scratch. -q quiet mode; no output -n train mail residing in "new" directory, in addition to "cur" directory, which is always trained (Maildir only) -r remove mail which was trained on (Maildir only) -o section:option:value set [section, option] in the options database to value """ import sys, os, getopt, email import shutil from spambayes import hammie, storage, mboxutils from spambayes.Options import options, get_pathname_option program = sys.argv[0] loud = True def get_message(obj): """Return an email Message object. This works like mboxutils.get_message, except it doesn't junk the headers if there's an error. Doing so would cause a headerless message to be written back out! """ if isinstance(obj, email.Message.Message): return obj # Create an email Message object. if hasattr(obj, "read"): obj = obj.read() try: msg = email.message_from_string(obj) except email.Errors.MessageParseError: msg = None return msg def msg_train(h, msg, is_spam, force): """Train bayes with a single message.""" # XXX: big hack -- why is email.Message unable to represent # multipart/alternative? try: mboxutils.as_string(msg) except TypeError: # We'll be unable to represent this as text :( return False if is_spam: spamtxt = options["Headers", "header_spam_string"] else: spamtxt = options["Headers", "header_ham_string"] oldtxt = msg.get(options["Headers", "trained_header_name"]) if force: # Train no matter what. if oldtxt != None: del msg[options["Headers", "trained_header_name"]] elif oldtxt == spamtxt: # Skip this one, we've already trained with it. return False elif oldtxt != None: # It's been trained, but as something else. Untrain. del msg[options["Headers", "trained_header_name"]] h.untrain(msg, not is_spam) h.train(msg, is_spam) msg.add_header(options["Headers", "trained_header_name"], spamtxt) return True def maildir_train(h, path, is_spam, force, removetrained): """Train bayes with all messages from a maildir.""" if loud: print " Reading %s as Maildir" % (path,) import time import socket pid = os.getpid() host = socket.gethostname() counter = 0 trained = 0 for fn in os.listdir(path): cfn = os.path.join(path, fn) tfn = os.path.normpath(os.path.join(path, "..", "tmp", "%d.%d_%d.%s" % (time.time(), pid, counter, host))) if (os.path.isdir(cfn)): continue counter += 1 if loud and counter % 10 == 0: sys.stdout.write("\r%6d" % counter) sys.stdout.flush() f = file(cfn, "rb") msg = get_message(f) f.close() if not msg: print "Malformed message: %s. Skipping..." % cfn continue if not msg_train(h, msg, is_spam, force): continue trained += 1 if not options["Headers", "include_trained"]: continue f = file(tfn, "wb") f.write(mboxutils.as_string(msg)) f.close() shutil.copystat(cfn, tfn) # XXX: This will raise an exception on Windows. Do any Windows # people actually use Maildirs? os.rename(tfn, cfn) if (removetrained): os.unlink(cfn) if loud: sys.stdout.write("\r%6d" % counter) sys.stdout.write("\r Trained %d out of %d messages\n" % (trained, counter)) def mbox_train(h, path, is_spam, force): """Train bayes with a Unix mbox""" if loud: print " Reading as Unix mbox" import mailbox import fcntl # Open and lock the mailbox. Some systems require it be opened for # writes in order to assert an exclusive lock. f = file(path, "r+b") fcntl.flock(f, fcntl.LOCK_EX) mbox = mailbox.PortableUnixMailbox(f, get_message) outf = os.tmpfile() counter = 0 trained = 0 for msg in mbox: if not msg: print "Malformed message number %d. I can't train on this mbox, sorry." % counter return counter += 1 if loud and counter % 10 == 0: sys.stdout.write("\r%6d" % counter) sys.stdout.flush() if msg_train(h, msg, is_spam, force): trained += 1 if options["Headers", "include_trained"]: # Write it out with the Unix "From " line outf.write(mboxutils.as_string(msg, True)) if options["Headers", "include_trained"]: outf.seek(0) try: os.ftruncate(f.fileno(), 0) f.seek(0) except: # If anything goes wrong, don't try to write print "Problem truncating mbox--nothing written" raise try: for line in outf.xreadlines(): f.write(line) except: print >> sys.stderr ("Problem writing mbox! Sorry, " "I tried my best, but your mail " "may be corrupted.") raise fcntl.flock(f, fcntl.LOCK_UN) f.close() if loud: sys.stdout.write("\r%6d" % counter) sys.stdout.write("\r Trained %d out of %d messages\n" % (trained, counter)) def mhdir_train(h, path, is_spam, force): """Train bayes with an mh directory""" if loud: print " Reading as MH mailbox" import glob counter = 0 trained = 0 for fn in glob.glob(os.path.join(path, "[0-9]*")): counter += 1 cfn = fn tfn = os.path.join(path, "spambayes.tmp") if loud and counter % 10 == 0: sys.stdout.write("\r%6d" % counter) sys.stdout.flush() f = file(fn, "rb") msg = get_message(f) f.close() if not msg: print "Malformed message: %s. Skipping..." % cfn continue msg_train(h, msg, is_spam, force) trained += 1 if not options["Headers", "include_trained"]: continue f = file(tfn, "wb") f.write(mboxutils.as_string(msg)) f.close() shutil.copystat(cfn, tfn) # XXX: This will raise an exception on Windows. Do any Windows # people actually use MH directories? os.rename(tfn, cfn) if loud: sys.stdout.write("\r%6d" % counter) sys.stdout.write("\r Trained %d out of %d messages\n" % (trained, counter)) def train(h, path, is_spam, force, trainnew, removetrained): if not os.path.exists(path): raise ValueError("Nonexistent path: %s" % path) elif os.path.isfile(path): mbox_train(h, path, is_spam, force) elif os.path.isdir(os.path.join(path, "cur")): maildir_train(h, os.path.join(path, "cur"), is_spam, force, removetrained) if trainnew: maildir_train(h, os.path.join(path, "new"), is_spam, force, removetrained) elif os.path.isdir(path): mhdir_train(h, path, is_spam, force) else: raise ValueError("Unable to determine mailbox type: " + path) def usage(code, msg=''): """Print usage message and sys.exit(code).""" if msg: print >> sys.stderr, msg print >> sys.stderr print >> sys.stderr, __doc__ % globals() sys.exit(code) def main(): """Main program; parse options and go.""" global loud try: opts, args = getopt.getopt(sys.argv[1:], 'hfqnrd:p:g:s:o:') except getopt.error, msg: usage(2, msg) if not opts: usage(2, "No options given") force = False trainnew = False removetrained = False good = [] spam = [] for opt, arg in opts: if opt == '-h': usage(0) elif opt == "-f": force = True elif opt == "-n": trainnew = True elif opt == "-q": loud = False elif opt == '-g': good.append(arg) elif opt == '-s': spam.append(arg) elif opt == "-r": removetrained = True elif opt == '-o': options.set_from_cmdline(arg, sys.stderr) pck, usedb = storage.database_type(opts) if args: usage(2, "Positional arguments not allowed") if usedb == None: # Use settings in configuration file. usedb = options["Storage", "persistent_use_database"] pck = get_pathname_option("Storage", "persistent_storage_file") h = hammie.open(pck, usedb, "c") for g in good: if loud: print "Training ham (%s):" % g train(h, g, False, force, trainnew, removetrained) sys.stdout.flush() save = True for s in spam: if loud: print "Training spam (%s):" % s train(h, s, True, force, trainnew, removetrained) sys.stdout.flush() save = True if save: h.store() if __name__ == "__main__": main() spambayes-1.1a6/scripts/sb_notesfilter.py0000664000076500000240000003543611116563065020763 0ustar skipstaff00000000000000#! /usr/bin/env python '''sb_notesfilter.py - Lotus Notes SpamBayes interface. This module uses SpamBayes as a filter against a Lotus Notes mail database. The Notes client must be running when this process is executed. It requires a Notes folder, named as a parameter, with four subfolders: Spam Ham Train as Spam Train as Ham Depending on the execution parameters, it will do any or all of the following steps, in the order given. 1. Train Spam from the Train as Spam folder (-t option) 2. Train Ham from the Train as Ham folder (-t option) 3. Replicate (-r option) 4. Classify the inbox (-c option) Mail that is to be trained as spam should be manually moved to that folder by the user. Likewise mail that is to be trained as ham. After training, spam is moved to the Spam folder and ham is moved to the Ham folder. Replication takes place if a remote server has been specified. This step may take a long time, depending on replication parameters and how much information there is to download, as well as line speed and server load. Please be patient if you run with replication. There is currently no progress bar or anything like that to tell you that it's working, but it is and will complete eventually. There is also no mechanism for notifying you that the replication failed. If it did, there is no harm done, and the program will continue execution. Mail that is classified as Spam is moved from the inbox to the Train as Spam folder. You should occasionally review your Spam folder for Ham that has mistakenly been classified as Spam. If there is any there, move it to the Train as Ham folder, so SpamBayes will be less likely to make this mistake again. Mail that is classified as Ham or Unsure is left in the inbox. There is currently no means of telling if a mail was classified as Ham or Unsure. You should occasionally select some Ham and move it to the Train as Ham folder, so Spambayes can tell the difference between Spam and Ham. The goal is to maintain an approximate balance between the number of Spam and the number of Ham that have been trained into the database. These numbers are reported every time this program executes. However, if the amount of Spam you receive far exceeds the amount of Ham you receive, it may be very difficult to maintain this balance. This is not a matter of great concern. SpamBayes will still make very few mistakes in this circumstance. But, if this is the case, you should review your Spam folder for falsely classified Ham, and retrain those that you find, on a regular basis. This will prevent statistical error accumulation, which if allowed to continue, would cause SpamBayes to tend to classify everything as Spam. Because there is no programmatic way to determine if a particular mail has been previously processed by this classification program, it keeps a pickled dictionary of notes mail ids, so that once a mail has been classified, it will not be classified again. The non-existence of this index file, named .sbindex, indicates to the system that this is an initialization execution. Rather than classify the inbox in this case, the contents of the inbox are placed in the index to note the 'starting point' of the system. After that, any new messages in the inbox are eligible for classification. Usage: sb_notesfilter [options] note: option values with spaces in them must be enclosed in double quotes options: -p dbname : pickled training database filename -d dbname : dbm training database filename -l dbname : database filename of local mail replica e.g. localmail.nsf -r server : server address of the server mail database e.g. d27ml602/27/M/IBM if specified, will initiate a replication -f folder : Name of SpamBayes folder must have subfolders: Spam Ham Train as Spam Train as Ham -t : train contents of Train as Spam and Train as Ham -c : classify inbox -h : help -P : prompt "Press Enter to end" before ending This is useful for automated executions where the statistics output would otherwise be lost when the window closes. -i filename : index file name -W : password -L dbname : log to database (template alog4.ntf) -o section:option:value : set [section, option] in the options database to value Examples: Replicate and classify inbox sb_notesfilter -c -d notesbayes -r mynoteserv -l mail.nsf -f Spambayes Train Spam and Ham, then classify inbox sb_notesfilter -t -c -d notesbayes -l mail.nsf -f Spambayes Replicate, then classify inbox sb_notesfilter -c -d test7 -l mail.nsf -r nynoteserv -f Spambayes To Do: o Dump/purge notesindex file o Create correct folders if they do not exist o Options for some of this stuff? o sb_server style training/configuration interface? o parameter to retrain? o Use spambayes.message MessageInfo db's rather than own database. o Suggestions? ''' # This module is part of the spambayes project, which is Copyright 2002-2007 # The Python Software Foundation and is covered by the Python Software # Foundation license. from __future__ import generators __author__ = "Tim Stone " __credits__ = "Mark Hammond, for his remarkable win32 modules." import sys import errno import getopt import win32com.client import pywintypes from spambayes import tokenizer, storage from spambayes.Options import options from spambayes.safepickle import pickle_read, pickle_write def classifyInbox(v, vmoveto, bayes, ldbname, notesindex, log): # the notesindex hash ensures that a message is looked at only once if len(notesindex.keys()) == 0: firsttime = 1 else: firsttime = 0 docstomove = [] numham = 0 numspam = 0 numuns = 0 numdocs = 0 doc = v.GetFirstDocument() while doc: nid = doc.NOTEID if firsttime: notesindex[nid] = 'never classified' else: if not notesindex.has_key(nid): numdocs += 1 # Notes returns strings in unicode, and the Python # decoder has trouble with these strings when # you try to print them. So don't... message = getMessage(doc) # generate_long_skips = True blows up on occasion, # probably due to this unicode problem. options["Tokenizer", "generate_long_skips"] = False tokens = tokenizer.tokenize(message) prob = bayes.spamprob(tokens) if prob < options["Categorization", "ham_cutoff"]: numham += 1 elif prob > options["Categorization", "spam_cutoff"]: docstomove += [doc] numspam += 1 else: numuns += 1 notesindex[nid] = 'classified' subj = message["subject"] try: print "%s spamprob is %s" % (subj[:30], prob) if log: log.LogAction("%s spamprob is %s" % (subj[:30], prob)) except UnicodeError: print " spamprob is %s" % (prob) if log: log.LogAction(" spamprob " \ "is %s" % (prob,)) item = doc.ReplaceItemValue("Spam", prob) item.IsSummary = True doc.save(False, True, False) doc = v.GetNextDocument(doc) # docstomove list is built because moving documents in the middle of # the classification loop loses the iterator position for doc in docstomove: doc.RemoveFromFolder(v.Name) doc.PutInFolder(vmoveto.Name) print "%s documents processed" % (numdocs,) print " %s classified as spam" % (numspam,) print " %s classified as ham" % (numham,) print " %s classified as unsure" % (numuns,) if log: log.LogAction("%s documents processed" % (numdocs,)) log.LogAction(" %s classified as spam" % (numspam,)) log.LogAction(" %s classified as ham" % (numham,)) log.LogAction(" %s classified as unsure" % (numuns,)) def getMessage(doc): try: subj = doc.GetItemValue('Subject')[0] except: subj = 'No Subject' try: body = doc.GetItemValue('Body')[0] except: body = 'No Body' hdrs = '' for item in doc.Items: if item.Name == "From" or item.Name == "Sender" or \ item.Name == "Received" or item.Name == "ReplyTo": try: hdrs = hdrs + ( "%s: %s\r\n" % (item.Name, item.Text) ) except: hdrs = '' message = "%sSubject: %s\r\n\r\n%s" % (hdrs, subj, body) return message def processAndTrain(v, vmoveto, bayes, is_spam, notesindex, log): if is_spam: header_str = options["Headers", "header_spam_string"] else: header_str = options["Headers", "header_ham_string"] print "Training %s" % (header_str,) docstomove = [] doc = v.GetFirstDocument() while doc: message = getMessage(doc) options["Tokenizer", "generate_long_skips"] = False tokens = tokenizer.tokenize(message) nid = doc.NOTEID if notesindex.has_key(nid): trainedas = notesindex[nid] if trainedas == options["Headers", "header_spam_string"] and \ not is_spam: # msg is trained as spam, is to be retrained as ham bayes.unlearn(tokens, True) elif trainedas == options["Headers", "header_ham_string"] and \ is_spam: # msg is trained as ham, is to be retrained as spam bayes.unlearn(tokens, False) bayes.learn(tokens, is_spam) notesindex[nid] = header_str docstomove += [doc] doc = v.GetNextDocument(doc) for doc in docstomove: doc.RemoveFromFolder(v.Name) doc.PutInFolder(vmoveto.Name) print "%s documents trained" % (len(docstomove),) if log: log.LogAction("%s documents trained" % (len(docstomove),)) def run(bdbname, useDBM, ldbname, rdbname, foldname, doTrain, doClassify, pwd, idxname, logname): bayes = storage.open_storage(bdbname, useDBM) try: notesindex = pickle_read(idxname) except IOError, e: if e.errno != errno.ENOENT: raise notesindex = {} print "%s file not found, this is a first time run" % (idxname,) print "No classification will be performed" need_replicate = False sess = win32com.client.Dispatch("Lotus.NotesSession") try: if pwd: sess.initialize(pwd) else: sess.initialize() except pywintypes.com_error: print "Session aborted" sys.exit() try: db = sess.GetDatabase(rdbname, ldbname) except pywintypes.com_error: if rdbname: print "Could not open database remotely, trying locally" try: db = sess.GetDatabase("", ldbname) need_replicate = True except pywintypes.com_error: print "Could not open database" sys.exit() else: raise log = sess.CreateLog("SpambayesAgentLog") try: log.OpenNotesLog("", logname) except pywintypes.com_error: print "Could not open log" log = None if log: log.LogAction("Running spambayes") vinbox = db.getView('($Inbox)') vspam = db.getView("%s\Spam" % (foldname,)) vham = db.getView("%s\Ham" % (foldname,)) vtrainspam = db.getView("%s\Train as Spam" % (foldname,)) vtrainham = db.getView("%s\Train as Ham" % (foldname,)) if doTrain: processAndTrain(vtrainspam, vspam, bayes, True, notesindex, log) # for some reason, using inbox as a target here loses the mail processAndTrain(vtrainham, vham, bayes, False, notesindex, log) if need_replicate: try: print "Replicating..." db.Replicate(rdbname) print "Done" except pywintypes.com_error: print "Could not replicate" if doClassify: classifyInbox(vinbox, vtrainspam, bayes, ldbname, notesindex, log) print "The Spambayes database currently has %s Spam and %s Ham" \ % (bayes.nspam, bayes.nham) bayes.store() pickle_write(idxname, notesindex) if log: log.LogAction("Finished running spambayes") if __name__ == '__main__': try: opts, args = getopt.getopt(sys.argv[1:], 'htcPd:p:l:r:f:o:i:W:L:') except getopt.error, msg: print >> sys.stderr, str(msg) + '\n\n' + __doc__ sys.exit() ldbname = None # local notes database name rdbname = None # remote notes database location sbfname = None # spambayes folder name idxname = None # index file name logname = None # log database name pwd = None # password doTrain = False doClassify = False doPrompt = False for opt, arg in opts: if opt == '-h': print >> sys.stderr, __doc__ sys.exit() elif opt == '-l': ldbname = arg elif opt == '-r': rdbname = arg elif opt == '-f': sbfname = arg elif opt == '-t': doTrain = True elif opt == '-c': doClassify = True elif opt == '-P': doPrompt = True elif opt == '-i': idxname = arg elif opt == '-L': logname = arg elif opt == '-W': pwd = arg elif opt == '-o': options.set_from_cmdline(arg, sys.stderr) bdbname, useDBM = storage.database_type(opts) if not idxname: idxname = "%s.sbindex" % (ldbname) if (bdbname and ldbname and sbfname and (doTrain or doClassify)): run(bdbname, useDBM, ldbname, rdbname, \ sbfname, doTrain, doClassify, pwd, idxname, logname) if doPrompt: raw_input("Press Enter to end ") else: print >> sys.stderr, __doc__ spambayes-1.1a6/scripts/sb_pop3dnd.py0000664000076500000240000011402111116631747017763 0ustar skipstaff00000000000000#!/usr/bin/env python """POP3DND - provides drag'n'drop training ability for POP3 clients. This application is a twisted cross between a POP3 proxy and an IMAP server. It sits between your mail client and your POP3 server (like any other POP3 proxy). While messages classified as ham are simply passed through the proxy, messages that are classified as spam or unsure are intercepted and passed to the IMAP server. The IMAP server offers four folders - one where messages classified as spam end up, one for messages it is unsure about, one for training ham, and one for training spam. In other words, to use this application, setup your mail client to connect to localhost, rather than directly to your POP3 server. Additionally, add a new IMAP account, also connecting to localhost. Setup the application via the web interface, and you are ready to go. Good messages will appear as per normal, but you will also have two new incoming folders, one for spam and one for unsure messages. To train SpamBayes, use the 'train_as_spam' and 'train_as_ham' folders. Any messages in these folders will be trained appropriately. This SpamBayes application is designed to work with Outlook Express, and provide the same sort of ease of use as the Outlook plugin. Although the majority of development and testing has been done with Outlook Express, Eudora and Thunderbird, any mail client that supports both IMAP and POP3 should be able to use this application - if the client enables the user to work with an IMAP account and POP3 account side-by-side (and move messages between them), then it should work equally as well. """ from __future__ import generators todo = """ o The RECENT flag should be unset at some point, but when? The RFC says that a message is recent if this is the first session to be notified about the message. Perhaps this can be done simply by *not* persisting this flag - i.e. the flag is always loaded as not recent, and only new messages are recent. The RFC says that if it is not possible to determine, then all messages should be recent, and this is what we currently do. o The Mailbox should be calling the appropriate listener functions (currently only newMessages is called on addMessage). flagsChanged should also be called on store, addMessage, or ??? o We cannot currently get part of a message via the BODY calls (with the <> operands), or get a part of a MIME message (by prepending a number). This should be added! o Suggestions? """ # This module is part of the spambayes project, which is Copyright 2002-2007 # The Python Software Foundation and is covered by the Python Software # Foundation license. __author__ = "Tony Meyer " __credits__ = "All the Spambayes folk." import os import re import sys import time import errno import email import thread import getopt import socket import imaplib import email.Utils import cStringIO as StringIO from twisted import cred import twisted.application.app from twisted.internet import defer from twisted.internet import reactor from twisted.internet.protocol import ServerFactory from twisted.protocols.imap4 import IMessage from twisted.protocols.imap4 import IAccount from twisted.protocols.imap4 import MessageSet from twisted.protocols.imap4 import IMAP4Server, MemoryAccount, IMailbox from twisted.protocols.imap4 import IMailboxListener from spambayes import storage from spambayes import message from spambayes.Stats import Stats from spambayes.Options import options from spambayes.tokenizer import tokenize from spambayes import FileCorpus, Dibbler from spambayes.Version import get_current_version from sb_server import POP3ProxyBase, State, _addressPortStr from spambayes.port import md5 def ensureDir(dirname): """Ensure that the given directory exists - in other words, if it does not exist, attempt to create it.""" try: os.mkdir(dirname) if options["globals", "verbose"]: print "Creating directory", dirname except OSError, e: if e.errno != errno.EEXIST: raise class IMAPMessage(message.Message): '''IMAP Message base class.''' __implements__ = (IMessage,) def __init__(self, date=None, message_db=None): message.Message.__init__(self, message_info_db=message_db) # We want to persist more information than the generic # Message class. self.stored_attributes.extend(["date", "deleted", "flagged", "seen", "draft", "recent", "answered"]) self.date = date self.clear_flags() # IMessage implementation def getHeaders(self, negate, *names): """Retrieve a group of message headers.""" headers = {} for header, value in self.items(): if (header.upper() in names and not negate) or \ (header.upper() not in names and negate) or names == (): headers[header.lower()] = value return headers def getFlags(self): """Retrieve the flags associated with this message.""" return self._flags_iter() def _flags_iter(self): if self.deleted: yield "\\DELETED" if self.answered: yield "\\ANSWERED" if self.flagged: yield "\\FLAGGED" if self.seen: yield "\\SEEN" if self.draft: yield "\\DRAFT" if self.recent: yield "\\RECENT" def getInternalDate(self): """Retrieve the date internally associated with this message.""" assert self.date is not None, \ "Must set date to use IMAPMessage instance." return self.date def getBodyFile(self): """Retrieve a file object containing the body of this message.""" # Note: only body, not headers! s = StringIO.StringIO() s.write(self.body()) s.seek(0) return s def getSize(self): """Retrieve the total size, in octets, of this message.""" return len(self.as_string()) def getUID(self): """Retrieve the unique identifier associated with this message.""" return self.id def isMultipart(self): """Indicate whether this message has subparts.""" return False def getSubPart(self, part): """Retrieve a MIME sub-message @type part: C{int} @param part: The number of the part to retrieve, indexed from 0. @rtype: Any object implementing C{IMessage}. @return: The specified sub-part. """ raise NotImplementedError # IMessage implementation ends def clear_flags(self): """Set all message flags to false.""" self.deleted = False self.answered = False self.flagged = False self.seen = False self.draft = False self.recent = False def set_flag(self, flag, value): # invalid flags are ignored flag = flag.upper() if flag == "\\DELETED": self.deleted = value elif flag == "\\ANSWERED": self.answered = value elif flag == "\\FLAGGED": self.flagged = value elif flag == "\\SEEN": self.seen = value elif flag == "\\DRAFT": self.draft = value else: print "Tried to set invalid flag", flag, "to", value def flags(self): """Return the message flags.""" return list(self._flags_iter()) def train(self, classifier, isSpam): if self.GetTrained() == (not isSpam): classifier.unlearn(self.tokenize(), not isSpam) self.RememberTrained(None) if self.GetTrained() is None: classifier.learn(self.tokenize(), isSpam) self.RememberTrained(isSpam) classifier.store() def structure(self, ext=False): """Body structure data describes the MIME-IMB format of a message and consists of a sequence of mime type, mime subtype, parameters, content id, description, encoding, and size. The fields following the size field are variable: if the mime type/subtype is message/rfc822, the contained message's envelope information, body structure data, and number of lines of text; if the mime type is text, the number of lines of text. Extension fields may also be included; if present, they are: the MD5 hash of the body, body disposition, body language.""" s = [] for part in self.walk(): if part.get_content_charset() is not None: charset = ("charset", part.get_content_charset()) else: charset = None part_s = [part.get_main_type(), part.get_subtype(), charset, part.get('Content-Id'), part.get('Content-Description'), part.get('Content-Transfer-Encoding'), str(len(part.as_string()))] #if part.get_type() == "message/rfc822": # part_s.extend([envelope, body_structure_data, # part.as_string().count("\n")]) #elif part.get_main_type() == "text": if part.get_main_type() == "text": part_s.append(str(part.as_string().count("\n"))) if ext: part_s.extend([md5(part.as_string()).digest(), part.get('Content-Disposition'), part.get('Content-Language')]) s.append(part_s) if len(s) == 1: return s[0] return s def body(self): rfc822 = self.as_string() bodyRE = re.compile(r"\r?\n(\r?\n)(.*)", re.DOTALL + re.MULTILINE) bmatch = bodyRE.search(rfc822) return bmatch.group(2) def headers(self): rfc822 = self.as_string() bodyRE = re.compile(r"\r?\n(\r?\n)(.*)", re.DOTALL + re.MULTILINE) bmatch = bodyRE.search(rfc822) return rfc822[:bmatch.start(2)] class DynamicIMAPMessage(IMAPMessage): """An IMAP Message that may change each time it is loaded.""" def __init__(self, func, mdb): date = imaplib.Time2Internaldate(time.time())[1:-1] IMAPMessage.__init__(self, date, mdb) self.func = func self.load() def load(self): # This only works for simple messages (non multi-part). self.set_payload(self.func(body=True)) # This only works for simple headers (no continuations). for headerstr in self.func(headers=True).split('\r\n'): header, value = headerstr.split(':') self[header] = value.strip() class IMAPFileMessage(IMAPMessage, FileCorpus.FileMessage): '''IMAP Message that persists as a file system artifact.''' def __init__(self, file_name=None, directory=None, mdb=None): """Constructor(message file name, corpus directory name).""" date = imaplib.Time2Internaldate(time.time())[1:-1] IMAPMessage.__init__(self, date, mdb) FileCorpus.FileMessage.__init__(self, file_name, directory) self.id = file_name class IMAPFileMessageFactory(FileCorpus.FileMessageFactory): '''MessageFactory for IMAPFileMessage objects''' def create(self, key, directory, content=None): '''Create a message object from a filename in a directory''' if content is None: return IMAPFileMessage(key, directory) msg = email.message_from_string(content, _class=IMAPFileMessage) msg.id = key msg.file_name = key msg.directory = directory return msg class IMAPMailbox(cred.perspective.Perspective): __implements__ = (IMailbox,) def __init__(self, name, identity_name, id): cred.perspective.Perspective.__init__(self, name, identity_name) self.UID_validity = id self.listeners = [] def getUIDValidity(self): """Return the unique validity identifier for this mailbox.""" return self.UID_validity def addListener(self, listener): """Add a mailbox change listener.""" self.listeners.append(listener) def removeListener(self, listener): """Remove a mailbox change listener.""" self.listeners.remove(listener) class SpambayesMailbox(IMAPMailbox): def __init__(self, name, id, directory): IMAPMailbox.__init__(self, name, "spambayes", id) self.UID_validity = id ensureDir(directory) self.storage = FileCorpus.FileCorpus(IMAPFileMessageFactory(), directory, r"[0123456789]*") # UIDs are required to be strictly ascending. if len(self.storage.keys()) == 0: self.nextUID = 1 else: self.nextUID = long(self.storage.keys()[-1]) + 1 # Calculate initial recent and unseen counts self.unseen_count = 0 self.recent_count = 0 for msg in self.storage: if not msg.seen: self.unseen_count += 1 if msg.recent: self.recent_count += 1 def getUIDNext(self, increase=False): """Return the likely UID for the next message added to this mailbox.""" reply = str(self.nextUID) if increase: self.nextUID += 1 return reply def getUID(self, msg): """Return the UID of a message in the mailbox.""" # Note that IMAP messages are 1-based, our messages are 0-based. d = self.storage return long(d.keys()[msg - 1]) def getFlags(self): """Return the flags defined in this mailbox.""" return ["\\Answered", "\\Flagged", "\\Deleted", "\\Seen", "\\Draft"] def getMessageCount(self): """Return the number of messages in this mailbox.""" return len(self.storage.keys()) def getRecentCount(self): """Return the number of messages with the 'Recent' flag.""" return self.recent_count def getUnseenCount(self): """Return the number of messages with the 'Unseen' flag.""" return self.unseen_count def isWriteable(self): """Get the read/write status of the mailbox.""" return True def destroy(self): """Called before this mailbox is deleted, permanently.""" # Our mailboxes cannot be deleted raise NotImplementedError def getHierarchicalDelimiter(self): """Get the character which delimits namespaces for in this mailbox.""" return '.' def requestStatus(self, names): """Return status information about this mailbox.""" answer = {} for request in names: request = request.upper() if request == "MESSAGES": answer[request] = self.getMessageCount() elif request == "RECENT": answer[request] = self.getRecentCount() elif request == "UIDNEXT": answer[request] = self.getUIDNext() elif request == "UIDVALIDITY": answer[request] = self.getUIDValidity() elif request == "UNSEEN": answer[request] = self.getUnseenCount() return answer def addMessage(self, content, flags=(), date=None): """Add the given message to this mailbox.""" msg = self.storage.makeMessage(self.getUIDNext(True), content.read()) msg.date = date self.storage.addMessage(msg) self.store(MessageSet(long(msg.id), long(msg.id)), flags, 1, True) msg.recent = True msg.store() self.recent_count += 1 self.unseen_count += 1 for listener in self.listeners: listener.newMessages(self.getMessageCount(), self.getRecentCount()) d = defer.Deferred() reactor.callLater(0, d.callback, self.storage.keys().index(msg.id)) return d def expunge(self): """Remove all messages flagged \\Deleted.""" deleted_messages = [] for msg in self.storage: if msg.deleted: if not msg.seen: self.unseen_count -= 1 if msg.recent: self.recent_count -= 1 deleted_messages.append(long(msg.id)) self.storage.removeMessage(msg) if deleted_messages != []: for listener in self.listeners: listener.newMessages(self.getMessageCount(), self.getRecentCount()) return deleted_messages def search(self, query, uid): """Search for messages that meet the given query criteria. @type query: C{list} @param query: The search criteria @rtype: C{list} @return: A list of message sequence numbers or message UIDs which match the search criteria. """ if self.getMessageCount() == 0: return [] all_msgs = MessageSet(long(self.storage.keys()[0]), long(self.storage.keys()[-1])) matches = [] for id, msg in self._messagesIter(all_msgs, uid): for q in query: if msg.matches(q): matches.append(id) break return matches def _messagesIter(self, messages, uid): if uid: if not self.storage.keys(): return messages.last = long(self.storage.keys()[-1]) else: messages.last = self.getMessageCount() for id in messages: if uid: msg = self.storage.get(str(id)) else: msg = self.storage.get(str(self.getUID(id))) if msg is None: # Non-existant message. continue # Load the message, if necessary if hasattr(msg, "load"): msg.load() yield (id, msg) def fetch(self, messages, uid): """Retrieve one or more messages.""" return self._messagesIter(messages, uid) def store(self, messages, flags, mode, uid): """Set the flags of one or more messages.""" stored_messages = {} for id, msg in self._messagesIter(messages, uid): if mode == 0: msg.clear_flags() value = True elif mode == -1: value = False elif mode == 1: value = True for flag in flags or (): # flags might be None if flag == '(' or flag == ')': continue if flag == "SEEN" and value == True and msg.seen == False: self.unseen_count -= 1 if flag == "SEEN" and value == False and msg.seen == True: self.unseen_count += 1 msg.set_flag(flag, value) stored_messages[id] = msg.flags() return stored_messages class SpambayesInbox(SpambayesMailbox): """A special mailbox that holds status messages from SpamBayes.""" def __init__(self, id, state): SpambayesMailbox.__init__(self, "INBOX", "spambayes", id) self.mdb = state.mdb self.UID_validity = id self.nextUID = 1 self.unseen_count = 0 self.recent_count = 0 self.storage = {} self.createMessages() self.stats = state.stats def buildStatusMessage(self, body=False, headers=False): """Build a message containing the current status message. If body is True, then return the body; if headers is True return the headers. If both are true, then return both (and insert a newline between them). """ msg = [] if headers: msg.append("Subject: SpamBayes Status") msg.append('From: "SpamBayes" ') if body: msg.append('\r\n') if body: state.buildStatusStrings() msg.append("POP3 proxy running on %s, proxying to %s." % \ (state.proxyPortsString, state.serversString)) msg.append("Active POP3 conversations: %s." % \ (state.activeSessions,)) msg.append("POP3 conversations this session: %s." % \ (state.totalSessions,)) msg.append("IMAP server running on %s." % \ (state.serverPortString,)) msg.append("Active IMAP4 conversations: %s." % \ (state.activeIMAPSessions,)) msg.append("IMAP4 conversations this session: %s." % \ (state.totalIMAPSessions,)) msg.append("Emails classified this session: %s spam, %s ham, " "%s unsure." % (state.numSpams, state.numHams, state.numUnsure)) msg.append("Total emails trained: Spam: %s Ham: %s" % \ (state.bayes.nspam, state.bayes.nham)) msg.append(state.warning or "SpamBayes is operating correctly.\r\n") return "\r\n".join(msg) def buildStatisticsMessage(self, body=False, headers=False): """Build a mesasge containing the current statistics. If body is True, then return the body; if headers is True return the headers. If both are true, then return both (and insert a newline between them). """ msg = [] if headers: msg.append("Subject: SpamBayes Statistics") msg.append('From: "SpamBayes" \r\n\r\n' \ '%s\r\nSee .\r\n' % (__doc__,) date = imaplib.Time2Internaldate(time.time())[1:-1] msg = email.message_from_string(about, _class=IMAPMessage) msg.date = date self.addMessage(msg) msg = DynamicIMAPMessage(self.buildStatusMessage, self.mdb) self.addMessage(msg) msg = DynamicIMAPMessage(self.buildStatisticsMessage, self.mdb) self.addMessage(msg) # XXX Add other messages here, for example # XXX help and other documentation. def isWriteable(self): """Get the read/write status of the mailbox.""" return False def addMessage(self, msg, flags=(), date=None): """Add the given message to this mailbox.""" msg.id = self.getUIDNext(True) self.storage[msg.id] = msg d = defer.Deferred() reactor.callLater(0, d.callback, self.storage.keys().index(msg.id)) return d def expunge(self): """Remove all messages flagged \\Deleted.""" # Mailbox is read-only. return [] def store(self, messages, flags, mode, uid): """Set the flags of one or more messages.""" # Mailbox is read-only. return {} class Trainer(object): """Listens to a given mailbox and trains new messages as spam or ham.""" __implements__ = (IMailboxListener,) def __init__(self, mailbox, asSpam): self.mailbox = mailbox self.asSpam = asSpam def modeChanged(self, writeable): # We don't care pass def flagsChanged(self, newFlags): # We don't care pass def newMessages(self, exists, recent): # We don't get passed the actual message, or the id of # the message, or even the message number. We just get # the total number of new/recent messages. # However, this function should be called _every_ time # that a new message appears, so we should be able to # assume that the last message is the new one. # (We ignore the recent count) if exists is not None: id = self.mailbox.getUID(exists) msg = self.mailbox.storage[str(id)] msg.train(state.bayes, self.asSpam) class SpambayesAccount(MemoryAccount): """Account for Spambayes server.""" def __init__(self, id, ham, spam, unsure, train_spam, inbox): MemoryAccount.__init__(self, id) self.mailboxes = {"SPAM" : spam, "UNSURE" : unsure, "TRAIN_AS_HAM" : ham, "TRAIN_AS_SPAM" : train_spam, "INBOX" : inbox} def select(self, name, readwrite=1): # 'INBOX' is a special case-insensitive name meaning the # primary mailbox for the user; for our purposes this contains # special messages from SpamBayes. return MemoryAccount.select(self, name, readwrite) class SpambayesIMAPServer(IMAP4Server): IDENT = "Spambayes IMAP Server IMAP4rev1 Ready" def __init__(self, user_account): IMAP4Server.__init__(self) self.account = user_account def authenticateLogin(self, user, passwd): """Lookup the account associated with the given parameters.""" if user == options["imapserver", "username"] and \ passwd == options["imapserver", "password"]: return (IAccount, self.account, None) raise cred.error.UnauthorizedLogin() def connectionMade(self): state.activeIMAPSessions += 1 state.totalIMAPSessions += 1 IMAP4Server.connectionMade(self) def connectionLost(self, reason): state.activeIMAPSessions -= 1 IMAP4Server.connectionLost(self, reason) def do_CREATE(self, tag, args): """Creating new folders on the server is not permitted.""" self.sendNegativeResponse(tag, \ "Creation of new folders is not permitted") auth_CREATE = (do_CREATE, IMAP4Server.arg_astring) select_CREATE = auth_CREATE def do_DELETE(self, tag, args): """Deleting folders on the server is not permitted.""" self.sendNegativeResponse(tag, \ "Deletion of folders is not permitted") auth_DELETE = (do_DELETE, IMAP4Server.arg_astring) select_DELETE = auth_DELETE class OneParameterFactory(ServerFactory): """A factory that allows a single parameter to be passed to the created protocol.""" def buildProtocol(self, addr): """Create an instance of a subclass of Protocol, passing a single parameter.""" if self.parameter is not None: p = self.protocol(self.parameter) else: p = self.protocol() p.factory = self return p class RedirectingBayesProxy(POP3ProxyBase): """Proxies between an email client and a POP3 server, redirecting mail to the imap server as necessary. It acts on the following POP3 commands: o RETR: o Adds the judgement header based on the raw headers and body of the message. """ # This message could be a bit more informative - it could at least # say whether it's the spam or unsure folder. It could give # information about who the message was from, or what the subject # was, if people thought that would be a good idea. intercept_message = 'From: "Spambayes" \r\n' \ 'Subject: Spambayes Intercept\r\n\r\nA message ' \ 'was intercepted by Spambayes (it scored %s).\r\n' \ '\r\nYou may find it in the Spam or Unsure ' \ 'folder.\r\n\r\n' def __init__(self, clientSocket, serverName, serverPort, spam, unsure): POP3ProxyBase.__init__(self, clientSocket, serverName, serverPort) self.handlers = {'RETR': self.onRetr} state.totalSessions += 1 state.activeSessions += 1 self.isClosed = False self.spam_folder = spam self.unsure_folder = unsure def send(self, data): """Logs the data to the log file.""" if options["globals", "verbose"]: state.logFile.write(data) state.logFile.flush() try: return POP3ProxyBase.send(self, data) except socket.error: self.close() def recv(self, size): """Logs the data to the log file.""" data = POP3ProxyBase.recv(self, size) if options["globals", "verbose"]: state.logFile.write(data) state.logFile.flush() return data def close(self): # This can be called multiple times by async. if not self.isClosed: self.isClosed = True state.activeSessions -= 1 POP3ProxyBase.close(self) def onTransaction(self, command, args, response): """Takes the raw request and response, and returns the (possibly processed) response to pass back to the email client. """ handler = self.handlers.get(command, self.onUnknown) return handler(command, args, response) def onRetr(self, command, args, response): """Classifies the message. If the result is ham, then simply pass it through. If the result is an unsure or spam, move it to the appropriate IMAP folder.""" # XXX This is all almost from sb_server! We could just # XXX extract that out into a function and call it here. # Use '\n\r?\n' to detect the end of the headers in case of # broken emails that don't use the proper line separators. if re.search(r'\n\r?\n', response): # Remove the trailing .\r\n before passing to the email parser. # Thanks to Scott Schlesier for this fix. terminatingDotPresent = (response[-4:] == '\n.\r\n') if terminatingDotPresent: response = response[:-3] # Break off the first line, which will be '+OK'. ok, messageText = response.split('\n', 1) try: msg = email.message_from_string(messageText, _class=message.SBHeaderMessage) # Now find the spam disposition and add the header. (prob, clues) = state.bayes.spamprob(msg.tokenize(), evidence=True) # Note that the X-SpamBayes-MailID header will be worthless # because we don't know the message id at this point. It's # not necessary for anything anyway, so just don't set the # [Headers] add_unique_id option. msg.addSBHeaders(prob, clues) # Check for "RETR" or "TOP N 99999999" - fetchmail without # the 'fetchall' option uses the latter to retrieve messages. if (command == 'RETR' or (command == 'TOP' and len(args) == 2 and args[1] == '99999999')): cls = msg.GetClassification() dest_folder = None if cls == options["Headers", "header_ham_string"]: state.numHams += 1 headers = [] for name, value in msg.items(): header = "%s: %s" % (name, value) headers.append(re.sub(r'\r?\n', '\r\n', header)) body = re.split(r'\n\r?\n', messageText, 1)[1] messageText = "\r\n".join(headers) + "\r\n\r\n" + body elif prob > options["Categorization", "spam_cutoff"]: dest_folder = self.spam_folder state.numSpams += 1 else: dest_folder = self.unsure_folder state.numUnsure += 1 if dest_folder: msg = StringIO.StringIO(msg.as_string()) date = imaplib.Time2Internaldate(time.time())[1:-1] dest_folder.addMessage(msg, (), date) # We have to return something, because the client # is expecting us to. We return a short message # indicating that a message was intercepted. messageText = self.intercept_message % (prob,) except: messageText, details = \ message.insert_exception_header(messageText) # Print the exception and a traceback. print >> sys.stderr, details retval = ok + "\n" + messageText if terminatingDotPresent: retval += '.\r\n' return retval else: # Must be an error response. return response def onUnknown(self, command, args, response): """Default handler; returns the server's response verbatim.""" return response class RedirectingBayesProxyListener(Dibbler.Listener): """Listens for incoming email client connections and spins off RedirectingBayesProxy objects to serve them. """ def __init__(self, serverName, serverPort, proxyPort, spam, unsure): proxyArgs = (serverName, serverPort, spam, unsure) Dibbler.Listener.__init__(self, proxyPort, RedirectingBayesProxy, proxyArgs) print 'Listener on port %s is proxying %s:%d' % \ (_addressPortStr(proxyPort), serverName, serverPort) class IMAPState(State): def __init__(self): State.__init__(self) # Set up the extra statistics. self.totalIMAPSessions = 0 self.activeIMAPSessions = 0 def createWorkers(self): """There aren't many workers in an IMAP State - most of the work is done elsewhere. We do need to load the classifier, though, and build the status strings.""" # Load token and message databases. if not hasattr(self, "DBName"): self.DBName, self.useDB = storage.database_type([]) self.bayes = storage.open_storage(self.DBName, self.useDB) if not hasattr(self, "MBDName"): self.MDBName, self.useMDB = message.database_type() self.mdb = message.open_storage(self.MDBName, self.useMDB) # Load stats manager. self.stats = Stats(options, self.mdb) # Build status strings. self.buildStatusStrings() def buildServerStrings(self): """After the server details have been set up, this creates string versions of the details, for display in the Status panel.""" self.serverPortString = str(self.imap_port) # Also build proxy strings State.buildServerStrings(self) state = IMAPState() # =================================================================== # __main__ driver. # =================================================================== def prepare(): # Setup state, server, boxes, trainers and account. state.imap_port = options["imapserver", "port"] state.createWorkers() proxyListeners = [] spam_box = SpambayesMailbox("Spam", 0, options["Storage", "spam_cache"]) unsure_box = SpambayesMailbox("Unsure", 1, options["Storage", "unknown_cache"]) ham_train_box = SpambayesMailbox("TrainAsHam", 2, options["Storage", "ham_cache"]) # We don't have a third cache location in the directory, so make one up. spam_train_cache = os.path.join(options["Storage", "ham_cache"], "..", "spam_to_train") spam_train_box = SpambayesMailbox("TrainAsSpam", 3, spam_train_cache) inbox = SpambayesInbox(4, state) spam_trainer = Trainer(spam_train_box, True) ham_trainer = Trainer(ham_train_box, False) spam_train_box.addListener(spam_trainer) ham_train_box.addListener(ham_trainer) user_account = SpambayesAccount(options["imapserver", "username"], ham_train_box, spam_box, unsure_box, spam_train_box, inbox) # Add IMAP4 server. f = OneParameterFactory() f.protocol = SpambayesIMAPServer f.parameter = user_account reactor.listenTCP(state.imap_port, f) # Add POP3 proxy. for (server, serverPort), proxyPort in zip(state.servers, state.proxyPorts): listener = RedirectingBayesProxyListener(server, serverPort, proxyPort, spam_box, unsure_box) proxyListeners.append(listener) state.prepare() def start(): assert state.prepared, "Must prepare before starting" # The asyncore stuff doesn't play nicely with twisted (or vice-versa), # so put them in separate threads. thread.start_new_thread(Dibbler.run, ()) reactor.run() def stop(): # Save the classifier, although that should not be necessary. state.bayes.store() # Explicitly closing the db is a good idea, though. state.bayes.close() # Stop the POP3 proxy. if state.proxyPorts: killer = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: killer.connect(('localhost', state.proxyPorts[0][1])) killer.send('KILL\r\n') killer.close() except socket.error: # Well, we did our best to shut down gracefully. Warn the user # and just die when the thread we are in does. print "Could not shut down POP3 proxy gracefully." # Stop the IMAP4 server. reactor.stop() def run(): # Read the arguments. try: opts, args = getopt.getopt(sys.argv[1:], 'ho:') except getopt.error, msg: print >> sys.stderr, str(msg) + '\n\n' + __doc__ sys.exit() for opt, arg in opts: if opt == '-h': print >> sys.stderr, __doc__ sys.exit() elif opt == '-o': options.set_from_cmdline(arg, sys.stderr) # Let the user know what they are using... v = get_current_version() print v.get_long_version() from twisted.copyright import version as twisted_version print "Twisted version %s.\n" % (twisted_version,) # Setup everything. prepare() # Kick things off. start() if __name__ == "__main__": run() spambayes-1.1a6/scripts/sb_server.py0000664000076500000240000012653011116610362017720 0ustar skipstaff00000000000000#!/usr/bin/env python """The primary server for SpamBayes. Currently serves the web interface, and any configured POP3 and SMTP proxies. The POP3 proxy works with classifier.py, and adds a simple X-Spambayes-Classification header (ham/spam/unsure) to each incoming email. You point the proxy at your POP3 server, and configure your email client to collect mail from the proxy then filter on the added header. Usage: sb_server.py [options] [ []] is the name of your real POP3 server is the port number of your real POP3 server, which defaults to 110. options: -h : Displays this help message. -d FILE : use the named DBM database file -p FILE : the the named Pickle database file -l port : proxy listens on this port number (default 110) -u port : User interface listens on this port number (default 8880; Browse http://localhost:8880/) -b : Launch a web browser showing the user interface. -o section:option:value : set [section, option] in the options database to value All command line arguments and switches take their default values from the [pop3proxy] and [html_ui] sections of bayescustomize.ini. For safety, and to help debugging, the whole POP3 conversation is written out to _pop3proxy.log for each run, if options["globals", "verbose"] is True. To make rebuilding the database easier, uploaded messages are appended to _pop3proxyham.mbox and _pop3proxyspam.mbox. """ # This module is part of the spambayes project, which is Copyright 2002-2007 # The Python Software Foundation and is covered by the Python Software # Foundation license. __author__ = "Richie Hindle " __credits__ = "Tim Peters, Neale Pickett, Tim Stone, all the Spambayes folk." todo = """ Web training interface: User interface improvements: o Once the pieces are on separate pages, make the paste box bigger. o Deployment: Windows executable? atlaxwin and ctypes? Or just webbrowser? o "Reload database" button. New features: o Online manual. o Links to project homepage, mailing list, etc. o List of words with stats (it would have to be paged!) a la SpamSieve. Code quality: o Cope with the email client timing out and closing the connection. Info: o Slightly-wordy index page; intro paragraph for each page. o In both stats and training results, report nham and nspam. o "Links" section (on homepage?) to project homepage, mailing list, etc. Gimmicks: o Classify a web page given a URL. o Graphs. Of something. Who cares what? o NNTP proxy. """ import sys, re, getopt, time, socket, email from thread import start_new_thread import spambayes.message from spambayes import i18n from spambayes import Stats from spambayes import Dibbler from spambayes import storage from spambayes.FileCorpus import ExpiryFileCorpus from spambayes.FileCorpus import FileMessageFactory, GzipFileMessageFactory from spambayes.Options import options, get_pathname_option, _ from spambayes.UserInterface import UserInterfaceServer from spambayes.ProxyUI import ProxyUserInterface from spambayes.Version import get_current_version # Increase the stack size on MacOS X. Stolen from Lib/test/regrtest.py if sys.platform == 'darwin': try: import resource except ImportError: pass else: soft, hard = resource.getrlimit(resource.RLIMIT_STACK) newsoft = min(hard, max(soft, 1024*2048)) resource.setrlimit(resource.RLIMIT_STACK, (newsoft, hard)) # exception may be raised if we are already running and check such things. class AlreadyRunningException(Exception): pass # number to add to STAT length for each msg to fudge for spambayes headers HEADER_SIZE_FUDGE_FACTOR = 512 class ServerLineReader(Dibbler.BrighterAsyncChat): """An async socket that reads lines from a remote server and simply calls a callback with the data. The BayesProxy object can't connect to the real POP3 server and talk to it synchronously, because that would block the process.""" def __init__(self, serverName, serverPort, lineCallback, ssl=False, map=None): Dibbler.BrighterAsyncChat.__init__(self, map=map) self.lineCallback = lineCallback self.handled_exception = False self.request = '' self.set_terminator('\r\n') self.create_socket(socket.AF_INET, socket.SOCK_STREAM) # create_socket creates a non-blocking socket. This is not great, # because then socket.connect() will return errno 10035, because # connect takes time. We then don't know if the connect call # succeeded or not. With Python 2.4, this means that we will move # into asyncore.loop(), and if the connect does fail, have a # loop something like 'while True: log(error)', which fills up # stdout very fast. Non-blocking is also a problem for ssl sockets. self.socket.setblocking(1) try: self.connect((serverName, serverPort)) except socket.error, e: error = "Can't connect to %s:%d: %s" % (serverName, serverPort, e) # Some people have their system setup to check mail very # frequently, but without being clever enough to check whether # the network is available. If we continually print the # "can't connect" error, we use up lots of CPU and disk space. # To avoid this, if not verbose only print each distinct error # once per hour. # See also: [ 1113863 ] sb_tray eats all cpu time now = time.time() then = time.time() - 3600 if error not in state.reported_errors or \ options["globals", "verbose"] or \ state.reported_errors[error] < then: print >>sys.stderr, error # Record this error in the list of ones we have seen this # session. state.reported_errors[error] = now self.lineCallback('-ERR %s\r\n' % error) self.lineCallback('') # "The socket's been closed." self.close() else: if ssl: try: self.ssl_socket = socket.ssl(self.socket) except socket.sslerror, why: if why[0] == 1: # error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol' # Probably not SSL after all. print >> sys.stderr, "Can't use SSL" else: raise else: self.send = self.send_ssl self.recv = self.recv_ssl self.socket.setblocking(0) def send_ssl(self, data): return self.ssl_socket.write(data) def handle_expt(self): # Python 2.4's system of continuously pumping error messages # is stupid. Print an error once, and then ignore. if not self.handled_exception: print >> sys.stderr, "Unhandled exception in ServerLineReader" self.handled_exception = True def recv_ssl(self, buffer_size): try: data = self.ssl_socket.read(buffer_size) if not data: # a closed connection is indicated by signaling # a read condition, and having recv() return 0. self.handle_close() return '' else: return data except socket.sslerror, why: if why[0] == 6: # 'TLS/SSL connection has been closed' self.handle_close() return '' elif why[0] == 2: # 'The operation did not complete (read)' return '' else: raise def collect_incoming_data(self, data): self.request = self.request + data def found_terminator(self): self.lineCallback(self.request + '\r\n') self.request = '' def handle_close(self): self.lineCallback('') self.close() try: del self.ssl_socket except AttributeError: pass class POP3ProxyBase(Dibbler.BrighterAsyncChat): """An async dispatcher that understands POP3 and proxies to a POP3 server, calling `self.onTransaction(request, response)` for each transaction. Responses are not un-byte-stuffed before reaching self.onTransaction() (they probably should be for a totally generic POP3ProxyBase class, but BayesProxy doesn't need it and it would mean re-stuffing them afterwards). self.onTransaction() should return the response to pass back to the email client - the response can be the verbatim response or a processed version of it. The special command 'KILL' kills it (passing a 'QUIT' command to the server). """ def __init__(self, clientSocket, serverName, serverPort, ssl=False, map=Dibbler._defaultContext._map): Dibbler.BrighterAsyncChat.__init__(self, clientSocket) self.request = '' self.response = '' self.set_terminator('\r\n') self.command = '' # The POP3 command being processed... self.args = [] # ...and its arguments self.isClosing = False # Has the server closed the socket? self.seenAllHeaders = False # For the current RETR or TOP self.startTime = 0 # (ditto) if not self.onIncomingConnection(clientSocket): # We must refuse this connection, so pass an error back # to the mail client. self.push("-ERR Connection not allowed\r\n") self.close_when_done() return self.serverSocket = ServerLineReader(serverName, serverPort, self.onServerLine, ssl, map) def onIncomingConnection(self, clientSocket): """Checks the security settings.""" # Stolen from UserInterface.py remoteIP = clientSocket.getpeername()[0] trustedIPs = options["pop3proxy", "allow_remote_connections"] if trustedIPs == "*" or remoteIP == clientSocket.getsockname()[0]: return True trustedIPs = trustedIPs.replace('.', '\.').replace('*', '([01]?\d\d?|2[0-4]\d|25[0-5])') for trusted in trustedIPs.split(','): if re.search("^" + trusted + "$", remoteIP): return True return False def onTransaction(self, command, args, response): """Overide this. Takes the raw request and the response, and returns the (possibly processed) response to pass back to the email client. """ raise NotImplementedError def onServerLine(self, line): """A line of response has been received from the POP3 server.""" isFirstLine = not self.response self.response = self.response + line # Is this the line that terminates a set of headers? self.seenAllHeaders = self.seenAllHeaders or line in ['\r\n', '\n'] # Has the server closed its end of the socket? if not line: self.isClosing = True # If we're not processing a command, just echo the response. if not self.command: self.push(self.response) self.response = '' # Time out after some seconds (30 by default) for message-retrieval # commands if all the headers are down. The rest of the message # will proxy straight through. # See also [ 870524 ] Make the message-proxy timeout configurable if self.command in ['TOP', 'RETR'] and \ self.seenAllHeaders and time.time() > \ self.startTime + options["pop3proxy", "retrieval_timeout"]: self.onResponse() self.response = '' # If that's a complete response, handle it. elif not self.isMultiline() or line == '.\r\n' or \ (isFirstLine and line.startswith('-ERR')): self.onResponse() self.response = '' def isMultiline(self): """Returns True if the request should get a multiline response (assuming the response is positive). """ if self.command in ['USER', 'PASS', 'APOP', 'QUIT', 'STAT', 'DELE', 'NOOP', 'RSET', 'KILL']: return False elif self.command in ['RETR', 'TOP', 'CAPA']: return True elif self.command in ['LIST', 'UIDL']: return len(self.args) == 0 else: # Assume that an unknown command will get a single-line # response. This should work for errors and for POP-AUTH, # and is harmless even for multiline responses - the first # line will be passed to onTransaction and ignored, then the # rest will be proxied straight through. return False def collect_incoming_data(self, data): """Asynchat override.""" self.request = self.request + data def found_terminator(self): """Asynchat override.""" verb = self.request.strip().upper() if verb == 'KILL': self.socket.shutdown(2) self.close() raise SystemExit elif verb == 'CRASH': # For testing raise ZeroDivisionError self.serverSocket.push(self.request + '\r\n') if self.request.strip() == '': # Someone just hit the Enter key. self.command = '' self.args = [] else: # A proper command. splitCommand = self.request.strip().split() self.command = splitCommand[0].upper() self.args = splitCommand[1:] self.startTime = time.time() self.request = '' def onResponse(self): # There are some features, tested by clients using CAPA, # that we don't support. We strip them from the CAPA # response here, so that the client won't use them. for unsupported in ['PIPELINING', 'STLS', ]: unsupportedLine = r'(?im)^%s[^\n]*\n' % (unsupported,) self.response = re.sub(unsupportedLine, '', self.response) # Pass the request and the raw response to the subclass and # send back the cooked response. if self.response: cooked = self.onTransaction(self.command, self.args, self.response) self.push(cooked) # If onServerLine() decided that the server has closed its # socket, close this one when the response has been sent. if self.isClosing: self.close_when_done() # Reset. self.command = '' self.args = [] self.isClosing = False self.seenAllHeaders = False class BayesProxyListener(Dibbler.Listener): """Listens for incoming email client connections and spins off BayesProxy objects to serve them. """ def __init__(self, serverName, serverPort, proxyPort, ssl=False): proxyArgs = (serverName, serverPort, ssl) Dibbler.Listener.__init__(self, proxyPort, BayesProxy, proxyArgs) print 'Listener on port %s is proxying %s:%d' % \ (_addressPortStr(proxyPort), serverName, serverPort) class BayesProxy(POP3ProxyBase): """Proxies between an email client and a POP3 server, inserting judgement headers. It acts on the following POP3 commands: o STAT: o Adds the size of all the judgement headers to the maildrop size. o LIST: o With no message number: adds the size of an judgement header to the message size for each message in the scan listing. o With a message number: adds the size of an judgement header to the message size. o RETR: o Adds the judgement header based on the raw headers and body of the message. o TOP: o Adds the judgement header based on the raw headers and as much of the body as the TOP command retrieves. This can mean that the header might have a different value for different calls to TOP, or for calls to TOP vs. calls to RETR. I'm assuming that the email client will either not make multiple calls, or will cope with the headers being different. o USER: o Does no processing based on the USER command itself, but expires any old messages in the three caches. """ def __init__(self, clientSocket, serverName, serverPort, ssl=False): POP3ProxyBase.__init__(self, clientSocket, serverName, serverPort, ssl) self.handlers = {'STAT': self.onStat, 'LIST': self.onList, 'RETR': self.onRetr, 'TOP': self.onTop, 'USER': self.onUser} state.totalSessions += 1 state.activeSessions += 1 self.isClosed = False def send(self, data): """Logs the data to the log file.""" if options["globals", "verbose"]: state.logFile.write(data) state.logFile.flush() try: return POP3ProxyBase.send(self, data) except socket.error: # The email client has closed the connection - 40tude Dialog # does this immediately after issuing a QUIT command, # without waiting for the response. self.close() def recv(self, size): """Logs the data to the log file.""" data = POP3ProxyBase.recv(self, size) if options["globals", "verbose"]: state.logFile.write(data) state.logFile.flush() return data def close(self): # This can be called multiple times by async. if not self.isClosed: self.isClosed = True state.activeSessions -= 1 POP3ProxyBase.close(self) def onTransaction(self, command, args, response): """Takes the raw request and response, and returns the (possibly processed) response to pass back to the email client. """ handler = self.handlers.get(command, self.onUnknown) return handler(command, args, response) def onStat(self, command, args, response): """Adds the size of all the judgement headers to the maildrop size.""" match = re.search(r'^\+OK\s+(\d+)\s+(\d+)(.*)\r\n', response) if match: count = int(match.group(1)) size = int(match.group(2)) + HEADER_SIZE_FUDGE_FACTOR * count return '+OK %d %d%s\r\n' % (count, size, match.group(3)) else: return response def onList(self, command, args, response): """Adds the size of an judgement header to the message size(s).""" if response.count('\r\n') > 1: # Multiline: all lines but the first contain a message size. lines = response.split('\r\n') outputLines = [lines[0]] for line in lines[1:]: match = re.search(r'^(\d+)\s+(\d+)', line) if match: number = int(match.group(1)) size = int(match.group(2)) + HEADER_SIZE_FUDGE_FACTOR line = "%d %d" % (number, size) outputLines.append(line) return '\r\n'.join(outputLines) else: # Single line. match = re.search(r'^\+OK\s+(\d+)\s+(\d+)(.*)\r\n', response) if match: messageNumber = match.group(1) size = int(match.group(2)) + HEADER_SIZE_FUDGE_FACTOR trailer = match.group(3) return "+OK %s %s%s\r\n" % (messageNumber, size, trailer) else: return response def onRetr(self, command, args, response): """Adds the judgement header based on the raw headers and body of the message.""" # Previously, we used '\n\r?\n' to detect the end of the headers in # case of broken emails that don't use the proper line separators, # and if we couldn't find it, then we assumed that the response was # and error response and passed it unfiltered. However, if the # message doesn't contain the separator (malformed mail), then this # would mean the message was passed straight through the proxy. # Since all the content is then in the headers, this probably # doesn't do a spammer much good, but, just in case, we now just # check for "+OK" and assume no error response will be given if # that is (which seems reasonable). # Remove the trailing .\r\n before passing to the email parser. # Thanks to Scott Schlesier for this fix. terminatingDotPresent = (response[-4:] == '\n.\r\n') if terminatingDotPresent: response = response[:-3] # Break off the first line, which will be '+OK'. statusLine, messageText = response.split('\n', 1) statusData = statusLine.split() ok = statusData[0] if ok.strip().upper() != "+OK": # Must be an error response. Return unproxied. return response try: msg = email.message_from_string(messageText, _class=spambayes.message.SBHeaderMessage) msg.setId(state.getNewMessageName()) # Now find the spam disposition and add the header. (prob, clues) = state.bayes.spamprob(msg.tokenize(), evidence=True) msg.addSBHeaders(prob, clues) # Check for "RETR" or "TOP N 99999999" - fetchmail without # the 'fetchall' option uses the latter to retrieve messages. if (command == 'RETR' or (command == 'TOP' and len(args) == 2 and args[1] == '99999999')): cls = msg.GetClassification() state.RecordClassification(cls, prob) # Suppress caching of "Precedence: bulk" or # "Precedence: list" ham if the options say so. isSuppressedBulkHam = \ (cls == options["Headers", "header_ham_string"] and options["Storage", "no_cache_bulk_ham"] and msg.get('precedence') in ['bulk', 'list']) # Suppress large messages if the options say so. size_limit = options["Storage", "no_cache_large_messages"] isTooBig = size_limit > 0 and \ len(messageText) > size_limit # Cache the message. Don't pollute the cache with test # messages or suppressed bulk ham. if (not state.isTest and options["Storage", "cache_messages"] and not isSuppressedBulkHam and not isTooBig): # Write the message into the Unknown cache. makeMessage = state.unknownCorpus.makeMessage message = makeMessage(msg.getId(), msg.as_string()) state.unknownCorpus.addMessage(message) # We'll return the message with the headers added. We take # all the headers from the SBHeaderMessage, but take the body # directly from the POP3 conversation, because the # SBHeaderMessage might have "fixed" a partial message by # appending a closing boundary separator. Remember we can # be dealing with partial message here because of the timeout # code in onServerLine. headers = [] for name, value in msg.items(): header = "%s: %s" % (name, value) headers.append(re.sub(r'\r?\n', '\r\n', header)) try: body = re.split(r'\n\r?\n', messageText, 1)[1] except IndexError: # No separator, so no body. Bad message, but proxy it # through anyway (adding the missing separator). messageText = "\r\n".join(headers) + "\r\n\r\n" else: messageText = "\r\n".join(headers) + "\r\n\r\n" + body except: # Something nasty happened while parsing or classifying - # report the exception in a hand-appended header and recover. # This is one case where an unqualified 'except' is OK, 'cos # anything's better than destroying people's email... messageText, details = spambayes.message.\ insert_exception_header(messageText) # Print the exception and a traceback. print >> sys.stderr, details # Restore the +OK and the POP3 .\r\n terminator if there was one. retval = ok + "\n" + messageText if terminatingDotPresent: retval += '.\r\n' return retval def onTop(self, command, args, response): """Adds the judgement header based on the raw headers and as much of the body as the TOP command retrieves.""" # Easy (but see the caveat in BayesProxy.__doc__). return self.onRetr(command, args, response) def onUser(self, command, args, response): """Spins off three separate threads that expires any old messages in the three caches, but does not do any processing of the USER command itself.""" start_new_thread(state.spamCorpus.removeExpiredMessages, ()) start_new_thread(state.hamCorpus.removeExpiredMessages, ()) start_new_thread(state.unknownCorpus.removeExpiredMessages, ()) return response def onUnknown(self, command, args, response): """Default handler; returns the server's response verbatim.""" return response # Implementations of a mutex or other resource which can prevent # multiple servers starting at once. Platform specific as no reasonable # cross-platform solution exists (however, an old trick is to use a # directory for a mutex, as a "create/test" atomic API generally exists). # Will return a handle to be later closed, or may throw AlreadyRunningException def open_platform_mutex(mutex_name="SpamBayesServer"): if sys.platform.startswith("win"): try: import win32event, win32api, winerror, win32con import pywintypes, ntsecuritycon # ideally, the mutex name could include either the username, # or the munged path to the INI file - this would mean we # would allow multiple starts so long as they weren't for # the same user. However, as of now, the service version # is likely to start as a different user, so a single mutex # is best for now. # XXX - even if we do get clever with another mutex name, we # should consider still creating a non-exclusive # "SpamBayesServer" mutex, if for no better reason than so # an installer can check if we are running try: hmutex = win32event.CreateMutex(None, True, mutex_name) except win32event.error, details: # If another user has the mutex open, we get an "access denied" # error - this is still telling us what we need to know. if details[0] != winerror.ERROR_ACCESS_DENIED: raise raise AlreadyRunningException # mutex opened - now check if we actually created it. if win32api.GetLastError()==winerror.ERROR_ALREADY_EXISTS: win32api.CloseHandle(hmutex) raise AlreadyRunningException return hmutex except ImportError: # no win32all - no worries, just start pass return None def close_platform_mutex(mutex): if sys.platform.startswith("win"): if mutex is not None: mutex.Close() # This keeps the global state of the module - the command-line options, # statistics like how many mails have been classified, the handle of the # log file, the Classifier and FileCorpus objects, and so on. class State: def __init__(self): """Initialises the State object that holds the state of the app. The default settings are read from Options.py and bayescustomize.ini and are then overridden by the command-line processing code in the __main__ code below.""" self.logFile = None self.bayes = None self.platform_mutex = None self.prepared = False self.can_stop = True self.init() # Load up the other settings from Option.py / bayescustomize.ini self.uiPort = options["html_ui", "port"] self.launchUI = options["html_ui", "launch_browser"] self.gzipCache = options["Storage", "cache_use_gzip"] self.cacheExpiryDays = options["Storage", "cache_expiry_days"] self.runTestServer = False self.isTest = False def init(self): assert not self.prepared, "init after prepare, but before close" # Load the environment for translation. self.lang_manager = i18n.LanguageManager() # Set the system user default language. self.lang_manager.set_language(\ self.lang_manager.locale_default_lang()) # Set interface to use the user language in the configuration file. for language in reversed(options["globals", "language"]): # We leave the default in there as the last option, to fall # back on if necessary. self.lang_manager.add_language(language) if options["globals", "verbose"]: print "Asked to add languages: " + \ ", ".join(options["globals", "language"]) print "Set language to " + \ str(self.lang_manager.current_langs_codes) # Open the log file. if options["globals", "verbose"]: self.logFile = open('_pop3proxy.log', 'wb', 0) if not hasattr(self, "servers"): # Could have already been set via the command line. self.servers = [] if options["pop3proxy", "remote_servers"]: for server in options["pop3proxy", "remote_servers"]: server = server.strip() if server.find(':') > -1: server, port = server.split(':', 1) else: port = '110' self.servers.append((server, int(port))) if not hasattr(self, "proxyPorts"): # Could have already been set via the command line. self.proxyPorts = [] if options["pop3proxy", "listen_ports"]: splitPorts = options["pop3proxy", "listen_ports"] self.proxyPorts = map(_addressAndPort, splitPorts) if len(self.servers) != len(self.proxyPorts): print "pop3proxy_servers & pop3proxy_ports are different lengths!" sys.exit() # Remember reported errors. self.reported_errors = {} # Set up the statistics. self.totalSessions = 0 self.activeSessions = 0 self.numSpams = 0 self.numHams = 0 self.numUnsure = 0 # Unique names for cached messages - see `getNewMessageName()` below. self.lastBaseMessageName = '' self.uniquifier = 2 def close(self): assert self.prepared, "closed without being prepared!" self.servers = None if self.bayes is not None: # Only store a non-empty db. if self.bayes.nham != 0 and self.bayes.nspam != 0: state.bayes.store() self.bayes.close() self.bayes = None if self.mdb is not None: self.mdb.store() self.mdb.close() self.mdb = None spambayes.message.Message().message_info_db = None self.spamCorpus = self.hamCorpus = self.unknownCorpus = None self.spamTrainer = self.hamTrainer = None self.prepared = False close_platform_mutex(self.platform_mutex) self.platform_mutex = None def prepare(self, can_stop=True): """Do whatever needs to be done to prepare for running. If can_stop is False, then we may not let the user shut down the proxy - for example, running as a Windows service this should be the case.""" # If we can, prevent multiple servers from running at the same time. assert self.platform_mutex is None, "Should not already have the mutex" self.platform_mutex = open_platform_mutex() self.can_stop = can_stop # Do whatever we've been asked to do... self.createWorkers() self.prepared = True def buildServerStrings(self): """After the server details have been set up, this creates string versions of the details, for display in the Status panel.""" serverStrings = ["%s:%s" % (s, p) for s, p in self.servers] self.serversString = ', '.join(serverStrings) self.proxyPortsString = ', '.join(map(_addressPortStr, self.proxyPorts)) def buildStatusStrings(self): """Build the status message(s) to display on the home page of the web interface.""" nspam = self.bayes.nspam nham = self.bayes.nham if nspam > 10 and nham > 10: db_ratio = nham/float(nspam) if db_ratio > 5.0: self.warning = _("Warning: you have much more ham than " \ "spam - SpamBayes works best with " \ "approximately even numbers of ham and " \ "spam.") elif db_ratio < (1/5.0): self.warning = _("Warning: you have much more spam than " \ "ham - SpamBayes works best with " \ "approximately even numbers of ham and " \ "spam.") else: self.warning = "" elif nspam > 0 or nham > 0: self.warning = _("Database only has %d good and %d spam - " \ "you should consider performing additional " \ "training.") % (nham, nspam) else: self.warning = _("Database has no training information. " \ "SpamBayes will classify all messages as " \ "'unsure', ready for you to train.") # Add an additional warning message if the user's thresholds are # truly odd. spam_cut = options["Categorization", "spam_cutoff"] ham_cut = options["Categorization", "ham_cutoff"] if spam_cut < 0.5: self.warning += _("
    Warning: we do not recommend " \ "setting the spam threshold less than 0.5.") if ham_cut > 0.5: self.warning += _("
    Warning: we do not recommend " \ "setting the ham threshold greater than 0.5.") if ham_cut > spam_cut: self.warning += _("
    Warning: your ham threshold is " \ "higher than your spam threshold. " \ "Results are unpredictable.") def createWorkers(self): """Using the options that were initialised in __init__ and then possibly overridden by the driver code, create the Bayes object, the Corpuses, the Trainers and so on.""" print "Loading database...", if self.isTest: self.useDB = "pickle" self.DBName = '_pop3proxy_test.pickle' # This is never saved. if not hasattr(self, "DBName"): self.DBName, self.useDB = storage.database_type([]) self.bayes = storage.open_storage(self.DBName, self.useDB) self.mdb = spambayes.message.Message().message_info_db # Load stats manager. self.stats = Stats.Stats(options, self.mdb) self.buildStatusStrings() # Don't set up the caches and training objects when running the self-test, # so as not to clutter the filesystem. if not self.isTest: # Create/open the Corpuses. Use small cache sizes to avoid hogging # lots of memory. sc = get_pathname_option("Storage", "spam_cache") hc = get_pathname_option("Storage", "ham_cache") uc = get_pathname_option("Storage", "unknown_cache") map(storage.ensureDir, [sc, hc, uc]) if self.gzipCache: factory = GzipFileMessageFactory() else: factory = FileMessageFactory() age = options["Storage", "cache_expiry_days"]*24*60*60 self.spamCorpus = ExpiryFileCorpus(age, factory, sc, '[0123456789\-]*', cacheSize=20) self.hamCorpus = ExpiryFileCorpus(age, factory, hc, '[0123456789\-]*', cacheSize=20) self.unknownCorpus = ExpiryFileCorpus(age, factory, uc, '[0123456789\-]*', cacheSize=20) # Given that (hopefully) users will get to the stage # where they do not need to do any more regular training to # be satisfied with spambayes' performance, we expire old # messages from not only the trained corpora, but the unknown # as well. self.spamCorpus.removeExpiredMessages() self.hamCorpus.removeExpiredMessages() self.unknownCorpus.removeExpiredMessages() # Create the Trainers. self.spamTrainer = storage.SpamTrainer(self.bayes) self.hamTrainer = storage.HamTrainer(self.bayes) self.spamCorpus.addObserver(self.spamTrainer) self.hamCorpus.addObserver(self.hamTrainer) def getNewMessageName(self): # The message name is the time it arrived, with a uniquifier # appended if two arrive within one clock tick of each other. messageName = "%10.10d" % long(time.time()) if messageName == self.lastBaseMessageName: messageName = "%s-%d" % (messageName, self.uniquifier) self.uniquifier += 1 else: self.lastBaseMessageName = messageName self.uniquifier = 2 return messageName def RecordClassification(self, cls, score): """Record the classification in the session statistics. cls should match one of the options["Headers", "header_*_string"] values. score is the score the message received. """ if cls == options["Headers", "header_ham_string"]: self.numHams += 1 elif cls == options["Headers", "header_spam_string"]: self.numSpams += 1 else: self.numUnsure += 1 self.stats.RecordClassification(score) # Option-parsing helper functions def _addressAndPort(s): """Decode a string representing a port to bind to, with optional address.""" s = s.strip() if ':' in s: addr, port = s.split(':') return addr, int(port) else: return '', int(s) def _addressPortStr((addr, port)): """Encode a string representing a port to bind to, with optional address.""" if not addr: return str(port) else: return '%s:%d' % (addr, port) state = State() proxyListeners = [] def _createProxies(servers, proxyPorts): """Create BayesProxyListeners for all the given servers.""" for (server, serverPort), proxyPort in zip(servers, proxyPorts): ssl = options["pop3proxy", "use_ssl"] if ssl == "automatic": ssl = serverPort == 995 listener = BayesProxyListener(server, serverPort, proxyPort, ssl) proxyListeners.append(listener) def _recreateState(): # Close the existing listeners and create new ones. This won't # affect any running proxies - once a listener has created a proxy, # that proxy is then independent of it. # (but won't closing the database screw them?) for proxy in proxyListeners: proxy.close() del proxyListeners[:] if state.prepared: # Close the state (which saves if necessary) state.close() # And get a new one going. state = State() prepare() _createProxies(state.servers, state.proxyPorts) return state def main(servers, proxyPorts, uiPort, launchUI): """Runs the proxy forever or until a 'KILL' command is received or someone hits Ctrl+Break.""" _createProxies(servers, proxyPorts) httpServer = UserInterfaceServer(uiPort) proxyUI = ProxyUserInterface(state, _recreateState) httpServer.register(proxyUI) Dibbler.run(launchBrowser=launchUI) def prepare(can_stop=True): state.init() state.prepare(can_stop) # Launch any SMTP proxies. Note that if the user hasn't specified any # SMTP proxy information in their configuration, then nothing will # happen. from spambayes import smtpproxy servers, proxyPorts = smtpproxy.LoadServerInfo() proxyListeners.extend(smtpproxy.CreateProxies(servers, proxyPorts, smtpproxy.SMTPTrainer(state.bayes, state))) # setup info for the web interface state.buildServerStrings() def start(): # kick everything off assert state.prepared, "starting before preparing state" try: main(state.servers, state.proxyPorts, state.uiPort, state.launchUI) finally: state.close() def stop(): # Shutdown as though through the web UI. This will save the DB, allow # any open proxy connections to complete, etc. from urllib import urlopen, urlencode urlopen('http://localhost:%d/save' % state.uiPort, urlencode({'how': _('Save & shutdown')})).read() # =================================================================== # __main__ driver. # =================================================================== def run(): global state # Read the arguments. try: opts, args = getopt.getopt(sys.argv[1:], 'hbd:p:l:u:o:') except getopt.error, msg: print >> sys.stderr, str(msg) + '\n\n' + __doc__ sys.exit() for opt, arg in opts: if opt == '-h': print >> sys.stderr, __doc__ sys.exit() elif opt == '-b': state.launchUI = True # '-p' and '-d' are handled by the storage.database_type call # below, in case you are wondering why they are missing. elif opt == '-l': state.proxyPorts = [_addressAndPort(a) for a in arg.split(',')] elif opt == '-u': state.uiPort = int(arg) elif opt == '-o': options.set_from_cmdline(arg, sys.stderr) state.DBName, state.useDB = storage.database_type(opts) # Let the user know what they are using... v = get_current_version() print "%s\n" % (v.get_long_version("SpamBayes POP3 Proxy"),) if 0 <= len(args) <= 2: # Normal usage, with optional server name and port number. if len(args) == 1: state.servers = [(args[0], 110)] elif len(args) == 2: state.servers = [(args[0], int(args[1]))] # Default to listening on port 110 for command-line-specified servers. if len(args) > 0 and state.proxyPorts == []: state.proxyPorts = [('', 110)] try: prepare() except AlreadyRunningException: print >> sys.stderr, \ "ERROR: The proxy is already running on this machine." print >> sys.stderr, "Please stop the existing proxy and try again" return start() else: print >> sys.stderr, __doc__ if __name__ == '__main__': run() spambayes-1.1a6/scripts/sb_unheader.py0000664000076500000240000001044010646440131020177 0ustar skipstaff00000000000000#!/usr/bin/env python """ sb_unheader.py: cleans headers from email messages. By default, this removes SpamAssassin headers, specify a pattern with -p to supply new headers to remove. This is often needed because existing spamassassin headers can provide killer spam clues, for all the wrong reasons. """ import re import sys import os import glob import mailbox import email.Parser import email.Message import email.Generator import getopt def unheader(msg, pat): pat = re.compile(pat) for hdr in msg.keys(): if pat.match(hdr): del msg[hdr] # remain compatible with 2.2.1 - steal replace_header from 2.3 source class Message(email.Message.Message): def replace_header(self, _name, _value): """Replace a header. Replace the first matching header found in the message, retaining header order and case. If no matching header was found, a KeyError is raised. """ _name = _name.lower() for i, (k, v) in zip(range(len(self._headers)), self._headers): if k.lower() == _name: self._headers[i] = (k, _value) break else: raise KeyError, _name class Parser(email.Parser.HeaderParser): def __init__(self): email.Parser.Parser.__init__(self, Message) def deSA(msg): if msg['X-Spam-Status']: if msg['X-Spam-Status'].startswith('Yes'): pct = msg['X-Spam-Prev-Content-Type'] if pct: msg['Content-Type'] = pct pcte = msg['X-Spam-Prev-Content-Transfer-Encoding'] if pcte: msg['Content-Transfer-Encoding'] = pcte subj = re.sub(r'\*\*\*\*\*SPAM\*\*\*\*\* ', '', msg['Subject'] or "") if subj != msg["Subject"]: msg.replace_header("Subject", subj) body = msg.get_payload() newbody = [] at_start = 1 for line in body.splitlines(): if at_start and line.startswith('SPAM: '): continue elif at_start: at_start = 0 newbody.append(line) msg.set_payload("\n".join(newbody)) unheader(msg, "X-Spam-") def process_message(msg, dosa, pats): if pats is not None: unheader(msg, pats) if dosa: deSA(msg) def process_mailbox(f, dosa=1, pats=None): gen = email.Generator.Generator(sys.stdout, maxheaderlen=0) for msg in mailbox.PortableUnixMailbox(f, Parser().parse): process_message(msg, dosa, pats) gen.flatten(msg, unixfrom=1) def process_maildir(d, dosa=1, pats=None): parser = Parser() for fn in glob.glob(os.path.join(d, "cur", "*")): print ("reading from %s..." % fn), file = open(fn) msg = parser.parse(file) process_message(msg, dosa, pats) tmpfn = os.path.join(d, "tmp", os.path.basename(fn)) tmpfile = open(tmpfn, "w") print "writing to %s" % tmpfn gen = email.Generator.Generator(tmpfile, maxheaderlen=0) gen.flatten(msg, unixfrom=0) os.rename(tmpfn, fn) def usage(): print >> sys.stderr, "usage: unheader.py [ -p pat ... ] [ -s ] folder" print >> sys.stderr, "-p pat gives a regex pattern used to eliminate unwanted headers" print >> sys.stderr, "'-p pat' may be given multiple times" print >> sys.stderr, "-s tells not to remove SpamAssassin headers" print >> sys.stderr, "-d means treat folder as a Maildir" def main(args): headerpats = [] dosa = 1 ismbox = 1 try: opts, args = getopt.getopt(args, "p:shd") except getopt.GetoptError: usage() sys.exit(1) else: for opt, arg in opts: if opt == "-h": usage() sys.exit(0) elif opt == "-p": headerpats.append(arg) elif opt == "-s": dosa = 0 elif opt == "-d": ismbox = 0 pats = headerpats and "|".join(headerpats) or None if len(args) != 1: usage() sys.exit(1) if ismbox: f = file(args[0]) process_mailbox(f, dosa, pats) else: process_maildir(args[0], dosa, pats) if __name__ == "__main__": main(sys.argv[1:]) spambayes-1.1a6/scripts/sb_upload.py0000664000076500000240000001233011116605655017677 0ustar skipstaff00000000000000#!/usr/bin/env python """ Read a message or a mailbox file on standard input, upload it to a web server and write it to standard output. By default, this sends the message to the SpamBayes sb_server web interface, which will save the message in the 'unknown' cache, ready for you to classify it. It does not do any training, just saves it ready for you to classify (unless you use the -t switch). usage: %(progname)s [-h] [-n] [-s server] [-p port] [-r N] [-o section:option:value] [-t (ham|spam)] Options: -h, --help - print help and exit -n, --null - suppress writing to standard output (default %(null)s) -s, --server= - provide alternate web server (default %(server)s) -p, --port= - provide alternate server port (default %(port)s) -r, --prob= - feed the message to the trainer w/ prob N [0.0...1.0] -t, --train= - train the message (pass either 'ham' or 'spam') -o, --option= - set [section, option] in the options database to value """ import sys import httplib import mimetypes import getopt import random from spambayes.Options import options progname = sys.argv[0] __author__ = "Skip Montanaro " __credits__ = "Spambayes gang, Wade Leftwich" # appropriated verbatim from a recipe by Wade Leftwich in the Python # Cookbook: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306 def post_multipart(host, selector, fields, files): """ Post fields and files to an http host as multipart/form-data. fields is a sequence of (name, value) elements for regular form fields. files is a sequence of (name, filename, value) elements for data to be uploaded as files. Return the server's response page. """ content_type, body = encode_multipart_formdata(fields, files) h = httplib.HTTP(host) h.putrequest('POST', selector) h.putheader('content-type', content_type) h.putheader('content-length', str(len(body))) h.endheaders() h.send(body) h.getreply() return h.file.read() def encode_multipart_formdata(fields, files): """ fields is a sequence of (name, value) elements for regular form fields. files is a sequence of (name, filename, value) elements for data to be uploaded as files. Return (content_type, body) ready for httplib.HTTP instance """ BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' CRLF = '\r\n' L = [] for (key, value) in fields: L.append('--' + BOUNDARY) L.append('Content-Disposition: form-data; name="%s"' % key) L.append('') L.append(value) for (key, filename, value) in files: L.append('--' + BOUNDARY) L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) L.append('Content-Type: %s' % get_content_type(filename)) L.append('') L.append(value) L.append('--' + BOUNDARY + '--') L.append('') body = CRLF.join(L) content_type = 'multipart/form-data; boundary=%s' % BOUNDARY return content_type, body def get_content_type(filename): return mimetypes.guess_type(filename)[0] or 'application/octet-stream' def usage(*args): defaults = {} for d in args: defaults.update(d) print __doc__ % defaults def main(argv): null = False server = "localhost" port = options["html_ui", "port"] prob = 1.0 train_as = None try: opts, args = getopt.getopt(argv, "hns:p:r:t:o:", ["help", "null", "server=", "port=", "prob=", "train=", "option="]) except getopt.error: usage(globals(), locals()) sys.exit(1) for opt, arg in opts: if opt in ("-h", "--help"): usage(globals(), locals()) sys.exit(0) elif opt in ("-n", "--null"): null = True elif opt in ("-s", "--server"): server = arg elif opt in ("-p", "--port"): port = int(arg) elif opt in ("-r", "--prob"): n = float(arg) if n < 0.0 or n > 1.0: usage(globals(), locals()) sys.exit(1) prob = n elif opt in ("-t", "--train"): arg = arg.capitalize() if arg not in ("Ham", "Spam"): usage(globals(), locals()) sys.exit(1) train_as = arg elif opt in ('-o', '--option'): options.set_from_cmdline(arg, sys.stderr) if args: usage(globals(), locals()) sys.exit(1) data = sys.stdin.read() if not null: sys.stdout.write(data) if random.random() < prob: try: if train_as is not None: which_text = "Train as %s" % (train_as,) post_multipart("%s:%d" % (server, port), "/train", [("which", which_text), ("text", "")], [("file", "message.dat", data)]) else: post_multipart("%s:%d" % (server, port), "/upload", [], [('file', 'message.dat', data)]) except: # not an error if the server isn't responding pass if __name__ == "__main__": main(sys.argv[1:]) spambayes-1.1a6/scripts/sb_xmlrpcserver.py0000664000076500000240000000442211116605661021147 0ustar skipstaff00000000000000#! /usr/bin/env python # A server version of hammie.py """Usage: %(program)s [options] IP:PORT Where: -h show usage and exit -p FILE use pickle FILE as the persistent store. loads data from this file if it exists, and saves data to this file at the end. -d FILE use DBM store FILE as the persistent store. -o section:option:value set [section, option] in the options database to value IP IP address to bind (use 0.0.0.0 to listen on all IPs of this machine) PORT Port number to listen to. """ import getopt import sys import xmlrpclib import SimpleXMLRPCServer from spambayes import hammie, Options from spambayes import storage class ReusableSimpleXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer): allow_reuse_address = True program = sys.argv[0] # For usage(); referenced by docstring above class XMLHammie(hammie.Hammie): def score(self, msg, *extra): try: msg = msg.data except AttributeError: pass return hammie.Hammie.score(self, msg, *extra) def filter(self, msg, *extra): try: msg = msg.data except AttributeError: pass return xmlrpclib.Binary(hammie.Hammie.filter(self, msg, *extra)) def usage(code, msg=''): """Print usage message and sys.exit(code).""" if msg: print >> sys.stderr, msg print >> sys.stderr print >> sys.stderr, __doc__ sys.exit(code) def main(): """Main program; parse options and go.""" try: opts, args = getopt.getopt(sys.argv[1:], 'hd:p:o:') except getopt.error, msg: usage(2, msg) options = Options.options for opt, arg in opts: if opt == '-h': usage(0) elif opt == '-o': options.set_from_cmdline(arg, sys.stderr) dbname, usedb = storage.database_type(opts) if len(args) != 1: usage(2, "IP:PORT not specified") ip, port = args[0].split(":") port = int(port) bayes = storage.open_storage(dbname, usedb) h = XMLHammie(bayes) server = ReusableSimpleXMLRPCServer( (ip, port), SimpleXMLRPCServer.SimpleXMLRPCRequestHandler) server.register_instance(h) server.serve_forever() if __name__ == "__main__": main() spambayes-1.1a6/setup.py0000664000076500000240000001116111116606761015400 0ustar skipstaff00000000000000#!/usr/bin/env python import os import sys from setuptools import setup, find_packages if sys.version_info < (2, 4): print "Error: You need at least Python 2.4 to use SpamBayes." print "You're running version %s." % sys.version sys.exit(0) # Install from distutils.core import setup from spambayes import __version__ import distutils.command.install_scripts parent = distutils.command.install_scripts.install_scripts class install_scripts(parent): old_scripts=[ 'unheader', 'hammie', 'hammiecli', 'hammiesrv', 'hammiefilter', 'pop3proxy', 'smtpproxy', 'sb_smtpproxy', 'proxytee', 'dbExpImp', 'mboxtrain', 'imapfilter', 'notesfilter', ] def run(self): err = False for s in self.old_scripts: s = os.path.join(self.install_dir, s) for e in (".py", ".pyc", ".pyo"): if os.path.exists(s+e): print >> sys.stderr, "Error: old script", s+e, print >> sys.stderr, "still exists." err = True if err: print >>sys.stderr, "Do you want to delete these scripts? (y/n)" answer = raw_input("") if answer == "y": for s in self.old_scripts: s = os.path.join(self.install_dir, s) for e in (".py", ".pyc", ".pyo"): try: os.remove(s+e) print "Removed", s+e except OSError: pass return parent.run(self) import distutils.command.sdist sdist_parent = distutils.command.sdist.sdist class sdist(sdist_parent): """Like the standard sdist, but also prints out MD5 checksums and sizes for the created files, for convenience.""" def run(self): import md5 retval = sdist_parent.run(self) for archive in self.get_archive_files(): data = file(archive, "rb").read() print '\n', archive, "\n\tMD5:", md5.md5(data).hexdigest() print "\tLength:", len(data) return retval scripts=['scripts/sb_client.py', 'scripts/sb_dbexpimp.py', 'scripts/sb_evoscore.py', 'scripts/sb_filter.py', 'scripts/sb_bnfilter.py', 'scripts/sb_bnserver.py', 'scripts/sb_imapfilter.py', 'scripts/sb_mailsort.py', 'scripts/sb_mboxtrain.py', 'scripts/sb_notesfilter.py', 'scripts/sb_pop3dnd.py', 'scripts/sb_server.py', 'scripts/core_server.py', 'scripts/sb_unheader.py', 'scripts/sb_upload.py', 'scripts/sb_xmlrpcserver.py', 'scripts/sb_chkopts.py', ] if sys.platform == 'win32': # Also install the pop3proxy_service and pop3proxy_tray scripts. # pop3proxy_service is only needed for installation and removal, # but pop3proxy_tray needs to be used all the time. Neither is # any use on a non-win32 platform. scripts.append('windows/pop3proxy_service.py') scripts.append('windows/pop3proxy_tray.py') if sys.version_info >= (3, 0): lf_min_version = "0.6" else: lf_min_version = "0.2" setup( name='spambayes', version = __version__, description = "Spam classification system", author = "the spambayes project", author_email = "spambayes@python.org", url = "http://spambayes.sourceforge.net", install_requires = ["lockfile>=%s" % lf_min_version, "pydns>=2.0"], cmdclass = {'install_scripts': install_scripts, 'sdist': sdist, }, scripts=scripts, packages = [ 'spambayes', 'spambayes.resources', 'spambayes.core_resources', ], classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Environment :: Plugins', 'Environment :: Win32 (MS Windows)', 'License :: OSI Approved :: Python Software Foundation License', 'Operating System :: POSIX', 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft :: Windows :: Windows 95/98/2000', 'Operating System :: Microsoft :: Windows :: Windows NT/2000', 'Natural Language :: English', 'Programming Language :: Python', 'Programming Language :: C', 'Intended Audience :: End Users/Desktop', 'Topic :: Communications :: Email :: Filters', 'Topic :: Communications :: Email :: Post-Office :: POP3', 'Topic :: Communications :: Email :: Post-Office :: IMAP', ], ) spambayes-1.1a6/spambayes/0000775000076500000240000000000011355064627015656 5ustar skipstaff00000000000000spambayes-1.1a6/spambayes/__init__.py0000664000076500000240000000010411355064242017753 0ustar skipstaff00000000000000# package marker. __version__ = "1.1a6" __date__ = "April 1, 2010" spambayes-1.1a6/spambayes/cdb.py0000664000076500000240000001273411112150033016741 0ustar skipstaff00000000000000#! /usr/bin/env python """ Dan Bernstein's CDB implemented in Python see http://cr.yp.to/cdb.html """ from __future__ import generators import os import struct import mmap def uint32_unpack(buf): return struct.unpack('>= 8 u %= self.hslots u <<= 3 self.kpos = self.hpos + u while self.loop < self.hslots: buf = self.read(8, self.kpos) pos = uint32_unpack(buf[4:]) if not pos: raise KeyError self.loop += 1 self.kpos += 8 if self.kpos == self.hpos + (self.hslots << 3): self.kpos = self.hpos u = uint32_unpack(buf[:4]) if u == self.khash: buf = self.read(8, pos) u = uint32_unpack(buf[:4]) if u == len(key): if self.match(key, pos + 8): dlen = uint32_unpack(buf[4:]) dpos = pos + 8 + len(key) return self.read(dlen, dpos) raise KeyError def __getitem__(self, key): self.findstart() return self.findnext(key) def get(self, key, default=None): self.findstart() try: return self.findnext(key) except KeyError: return default def cdb_dump(infile): """dump a database in djb's cdbdump format""" db = Cdb(infile) for key, value in db.iteritems(): print "+%d,%d:%s->%s" % (len(key), len(value), key, value) print def cdb_make(outfile, items): pos = 2048 tables = {} # { h & 255 : [(h, p)] } # write keys and data outfile.seek(pos) for key, value in items: outfile.write(uint32_pack(len(key)) + uint32_pack(len(value))) h = cdb_hash(key) outfile.write(key) outfile.write(value) tables.setdefault(h & 255, []).append((h, pos)) pos += 8 + len(key) + len(value) final = '' # write hash tables for i in range(256): entries = tables.get(i, []) nslots = 2*len(entries) final += uint32_pack(pos) + uint32_pack(nslots) null = (0, 0) table = [null] * nslots for h, p in entries: n = (h >> 8) % nslots while table[n] is not null: n = (n + 1) % nslots table[n] = (h, p) for h, p in table: outfile.write(uint32_pack(h) + uint32_pack(p)) pos += 8 # write header (pointers to tables and their lengths) outfile.flush() outfile.seek(0) outfile.write(final) def test(): #db = Cdb(open("t")) #print db['one'] #print db['two'] #print db['foo'] #print db['us'] #print db.get('ec') #print db.get('notthere') db = open('test.cdb', 'wb') cdb_make(db, [('one', 'Hello'), ('two', 'Goodbye'), ('foo', 'Bar'), ('us', 'United States'), ]) db.close() db = Cdb(open("test.cdb", 'rb')) print db['one'] print db['two'] print db['foo'] print db['us'] print db.get('ec') print db.get('notthere') if __name__ == '__main__': test() spambayes-1.1a6/spambayes/cdb_classifier.py0000664000076500000240000000153110714210500021141 0ustar skipstaff00000000000000"""A classifier that uses a CDB database. A CDB wordinfo database is quite small and fast but is slow to update. It is appropriate if training is done rarely (e.g. monthly or weekly using archived ham and spam). See mailsort.py for an example application that uses this classifier. """ from spambayes import cdb from spambayes.classifier import Classifier class CdbClassifier(Classifier): def __init__(self, cdbfile=None): Classifier.__init__(self) if cdbfile is not None: self.wordinfo = cdb.Cdb(cdbfile) def probability(self, record): return float(record) def save_wordinfo(self, db_file): items = [] for word, record in self.wordinfo.iteritems(): prob = Classifier.probability(self, record) items.append((word, str(prob))) cdb.cdb_make(db_file, items) spambayes-1.1a6/spambayes/chi2.py0000664000076500000240000001225711116605675017063 0ustar skipstaff00000000000000import math as _math import random def chi2Q(x2, v, exp=_math.exp, min=min): """Return prob(chisq >= x2, with v degrees of freedom). v must be even. """ assert v & 1 == 0 # XXX If x2 is very large, exp(-m) will underflow to 0. m = x2 / 2.0 sum = term = exp(-m) for i in range(1, v//2): 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, chi2Q(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. return min(sum, 1.0) def normZ(z, sqrt2pi=_math.sqrt(2.0*_math.pi), exp=_math.exp): "Return value of the unit Gaussian at z." return exp(-z*z/2.0) / sqrt2pi def normP(z): """Return area under the unit Gaussian from -inf to z. This is the probability that a zscore is <= z. """ # This is very accurate in a fixed-point sense. For negative z of # large magnitude (<= -8.3), it returns 0.0, essentially because # P(-z) is, to machine precision, indistiguishable from 1.0 then. # sum <- area from 0 to abs(z). a = abs(float(z)) if a >= 8.3: sum = 0.5 else: sum2 = term = a * normZ(a) z2 = a*a sum = 0.0 i = 1.0 while sum != sum2: sum = sum2 i += 2.0 term *= z2 / i sum2 += term if z >= 0: result = 0.5 + sum else: result = 0.5 - sum return result def normIQ(p, sqrt=_math.sqrt, ln=_math.log): """Return z such that the area under the unit Gaussian from z to +inf is p. Must have 0.0 <= p <= 1.0. """ assert 0.0 <= p <= 1.0 # This is a low-accuracy rational approximation from Abramowitz & Stegun. # The absolute error is bounded by 3e-3. flipped = False if p > 0.5: flipped = True p = 1.0 - p if p == 0.0: z = 8.3 else: t = sqrt(-2.0 * ln(p)) z = t - (2.30753 + .27061*t) / (1. + .99229*t + .04481*t**2) if flipped: z = -z return z def normIP(p): """Return z such that the area under the unit Gaussian from -inf to z is p. Must have 0.0 <= p <= 1.0. """ z = normIQ(1.0 - p) # One Newton step should double the # of good digits. return z + (p - normP(z)) / normZ(z) def main(): from spambayes.Histogram import Hist import sys class WrappedRandom: # There's no way W-H is equidistributed in 50 dimensions, so use # Marsaglia-wrapping to shuffle it more. def __init__(self, baserandom=random.random, tabsize=513): self.baserandom = baserandom self.n = tabsize self.tab = [baserandom() for _i in range(tabsize)] self.next = baserandom() def random(self): result = self.next i = int(result * self.n) self.next = self.tab[i] self.tab[i] = self.baserandom() return result random = WrappedRandom().random #from uni import uni as random #print random def judge(ps, ln=_math.log, ln2=_math.log(2), frexp=_math.frexp): H = S = 1.0 Hexp = Sexp = 0 for p in ps: S *= 1.0 - p H *= p if S < 1e-200: S, e = frexp(S) Sexp += e if H < 1e-200: H, e = frexp(H) Hexp += e S = ln(S) + Sexp * ln2 H = ln(H) + Hexp * ln2 n = len(ps) S = 1.0 - chi2Q(-2.0 * S, 2*n) H = 1.0 - chi2Q(-2.0 * H, 2*n) return S, H, (S-H + 1.0) / 2.0 warp = 0 bias = 0.99 if len(sys.argv) > 1: warp = int(sys.argv[1]) if len(sys.argv) > 2: bias = float(sys.argv[2]) h = Hist(20, lo=0.0, hi=1.0) s = Hist(20, lo=0.0, hi=1.0) score = Hist(20, lo=0.0, hi=1.0) for _i in xrange(5000): ps = [random() for _j in xrange(50)] s1, h1, score1 = judge(ps + [bias] * warp) s.add(s1) h.add(h1) score.add(score1) print "Result for random vectors of 50 probs, +", warp, "forced to", bias # Should be uniformly distributed on all-random data. print print 'H', h.display() # Should be uniformly distributed on all-random data. print print 'S', s.display() # Distribution doesn't really matter. print print '(S-H+1)/2', score.display() def showscore(ps, ln=_math.log, ln2=_math.log(2), frexp=_math.frexp): H = S = 1.0 Hexp = Sexp = 0 for p in ps: S *= 1.0 - p H *= p if S < 1e-200: S, e = frexp(S) Sexp += e if H < 1e-200: H, e = frexp(H) Hexp += e S = ln(S) + Sexp * ln2 H = ln(H) + Hexp * ln2 n = len(ps) probS = chi2Q(-2*S, 2*n) probH = chi2Q(-2*H, 2*n) print "P(chisq >= %10g | v=%3d) = %10g" % (-2*S, 2*n, probS) print "P(chisq >= %10g | v=%3d) = %10g" % (-2*H, 2*n, probH) S = 1.0 - probS H = 1.0 - probH score = (S-H + 1.0) / 2.0 print "spam prob", S print " ham prob", H print "(S-H+1)/2", score if __name__ == '__main__': main() spambayes-1.1a6/spambayes/classifier.py0000664000076500000000000007634511116610460020351 0ustar skipwheel00000000000000#! /usr/bin/env python from __future__ import generators # An implementation of a Bayes-like spam classifier. # # Paul Graham's original description: # # http://www.paulgraham.com/spam.html # # A highly fiddled version of that can be retrieved from our CVS repository, # via tag Last-Graham. This made many demonstrated improvements in error # rates over Paul's original description. # # This code implements Gary Robinson's suggestions, the core of which are # well explained on his webpage: # # http://radio.weblogs.com/0101454/stories/2002/09/16/spamDetection.html # # This is theoretically cleaner, and in testing has performed at least as # well as our highly tuned Graham scheme did, often slightly better, and # sometimes much better. It also has "a middle ground", which people like: # the scores under Paul's scheme were almost always very near 0 or very near # 1, whether or not the classification was correct. The false positives # and false negatives under Gary's basic scheme (use_gary_combining) generally # score in a narrow range around the corpus's best spam_cutoff value. # However, it doesn't appear possible to guess the best spam_cutoff value in # advance, and it's touchy. # # The last version of the Gary-combining scheme can be retrieved from our # CVS repository via tag Last-Gary. # # The chi-combining scheme used by default here gets closer to the theoretical # basis of Gary's combining scheme, and does give extreme scores, but also # has a very useful middle ground (small # of msgs spread across a large range # of scores, and good cutoff values aren't touchy). # # This implementation is due to Tim Peters et alia. import math # XXX At time of writing, these are only necessary for the # XXX experimental url retrieving/slurping code. If that # XXX gets ripped out, either rip these out, or run # XXX PyChecker over the code. import re import os import sys import socket import urllib2 from email import message_from_string DOMAIN_AND_PORT_RE = re.compile(r"([^:/\\]+)(:([\d]+))?") HTTP_ERROR_RE = re.compile(r"HTTP Error ([\d]+)") URL_KEY_RE = re.compile(r"[\W]") # XXX ---- ends ---- from spambayes.Options import options from spambayes.chi2 import chi2Q from spambayes.safepickle import pickle_read, pickle_write LN2 = math.log(2) # used frequently by chi-combining slurp_wordstream = None PICKLE_VERSION = 5 class WordInfo(object): # A WordInfo is created for each distinct word. spamcount is the # number of trained spam msgs in which the word appears, and hamcount # the number of trained ham msgs. # # Invariant: For use in a classifier database, at least one of # spamcount and hamcount must be non-zero. # # Important: This is a tiny object. Use of __slots__ is essential # to conserve memory. __slots__ = 'spamcount', 'hamcount' def __init__(self): self.__setstate__((0, 0)) def __repr__(self): return "WordInfo" + repr((self.spamcount, self.hamcount)) def __getstate__(self): return self.spamcount, self.hamcount def __setstate__(self, t): self.spamcount, self.hamcount = t class Classifier: # Defining __slots__ here made Jeremy's life needlessly difficult when # trying to hook this all up to ZODB as a persistent object. There's # no space benefit worth getting from slots in this class; slots were # used solely to help catch errors earlier, when this code was changing # rapidly. #__slots__ = ('wordinfo', # map word to WordInfo record # 'nspam', # number of spam messages learn() has seen # 'nham', # number of non-spam messages learn() has seen # ) # allow a subclass to use a different class for WordInfo WordInfoClass = WordInfo def __init__(self): self.wordinfo = {} self.probcache = {} self.nspam = self.nham = 0 def __getstate__(self): return (PICKLE_VERSION, self.wordinfo, self.nspam, self.nham) def __setstate__(self, t): if t[0] != PICKLE_VERSION: raise ValueError("Can't unpickle -- version %s unknown" % t[0]) (self.wordinfo, self.nspam, self.nham) = t[1:] self.probcache = {} # spamprob() implementations. One of the following is aliased to # spamprob, depending on option settings. # Currently only chi-squared is available, but maybe there will be # an alternative again someday. # 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. def chi2_spamprob(self, wordstream, evidence=False): """Return best-guess probability that wordstream is spam. wordstream is an iterable object producing words. The return value is a float in [0.0, 1.0]. If optional arg evidence is True, the return value is a pair probability, evidence where evidence is a list of (word, probability) pairs. """ from math import frexp, log as ln # 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). # # For optimization, sum-of-logs == log-of-product, and f.p. # multiplication is a lot cheaper than calling ln(). It's easy # to underflow to 0.0, though, so we simulate unbounded dynamic # range via frexp. The real product H = this H * 2**Hexp, and # likewise the real product S = this S * 2**Sexp. H = S = 1.0 Hexp = Sexp = 0 clues = self._getclues(wordstream) for prob, word, record in clues: S *= 1.0 - prob H *= prob if S < 1e-200: # prevent underflow S, e = frexp(S) Sexp += e if H < 1e-200: # prevent underflow H, e = frexp(H) Hexp += e # Compute the natural log of the product = sum of the logs: # ln(x * 2**i) = ln(x) + i * ln(2). S = ln(S) + Sexp * LN2 H = ln(H) + Hexp * LN2 n = len(clues) if n: S = 1.0 - chi2Q(-2.0 * S, 2*n) H = 1.0 - chi2Q(-2.0 * H, 2*n) # 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.]. prob = (S-H + 1.0) / 2.0 else: prob = 0.5 if evidence: clues = [(w, p) for p, w, _r in clues] clues.sort(lambda a, b: cmp(a[1], b[1])) clues.insert(0, ('*S*', S)) clues.insert(0, ('*H*', H)) return prob, clues else: return prob def slurping_spamprob(self, wordstream, evidence=False): """Do the standard chi-squared spamprob, but if the evidence leaves the score in the unsure range, and we have fewer tokens than max_discriminators, also generate tokens from the text obtained by following http URLs in the message.""" h_cut = options["Categorization", "ham_cutoff"] s_cut = options["Categorization", "spam_cutoff"] # Get the raw score. prob, clues = self.chi2_spamprob(wordstream, True) # If necessary, enhance it with the tokens from whatever is # at the URL's destination. if len(clues) < options["Classifier", "max_discriminators"] and \ prob > h_cut and prob < s_cut and slurp_wordstream: slurp_tokens = list(self._generate_slurp()) slurp_tokens.extend([w for (w, _p) in clues]) sprob, sclues = self.chi2_spamprob(slurp_tokens, True) if sprob < h_cut or sprob > s_cut: prob = sprob clues = sclues if evidence: return prob, clues return prob if options["Classifier", "use_chi_squared_combining"]: if options["URLRetriever", "x-slurp_urls"]: spamprob = slurping_spamprob else: spamprob = chi2_spamprob def learn(self, wordstream, is_spam): """Teach the classifier by example. wordstream is a word stream representing a message. If is_spam is True, you're telling the classifier this message is definitely spam, else that it's definitely not spam. """ if options["Classifier", "use_bigrams"]: wordstream = self._enhance_wordstream(wordstream) if options["URLRetriever", "x-slurp_urls"]: wordstream = self._add_slurped(wordstream) self._add_msg(wordstream, is_spam) def unlearn(self, wordstream, is_spam): """In case of pilot error, call unlearn ASAP after screwing up. Pass the same arguments you passed to learn(). """ if options["Classifier", "use_bigrams"]: wordstream = self._enhance_wordstream(wordstream) if options["URLRetriever", "x-slurp_urls"]: wordstream = self._add_slurped(wordstream) self._remove_msg(wordstream, is_spam) def probability(self, record): """Compute, store, and return prob(msg is spam | msg contains word). This is the Graham calculation, but stripped of biases, and stripped of clamping into 0.01 thru 0.99. The Bayesian adjustment following keeps them in a sane range, and one that naturally grows the more evidence there is to back up a probability. """ spamcount = record.spamcount hamcount = record.hamcount # Try the cache first try: return self.probcache[spamcount][hamcount] except KeyError: pass nham = float(self.nham or 1) nspam = float(self.nspam or 1) assert hamcount <= nham, "Token seen in more ham than ham trained." hamratio = hamcount / nham assert spamcount <= nspam, "Token seen in more spam than spam trained." spamratio = spamcount / nspam prob = spamratio / (hamratio + spamratio) S = options["Classifier", "unknown_word_strength"] StimesX = S * options["Classifier", "unknown_word_prob"] # Now do Robinson's Bayesian adjustment. # # s*x + n*p(w) # f(w) = -------------- # s + n # # I find this easier to reason about like so (equivalent when # s != 0): # # x - p # p + ------- # 1 + n/s # # IOW, it moves p a fraction of the distance from p to x, and # less so the larger n is, or the smaller s is. n = hamcount + spamcount prob = (StimesX + n * prob) / (S + n) # Update the cache try: self.probcache[spamcount][hamcount] = prob except KeyError: self.probcache[spamcount] = {hamcount: prob} return prob # NOTE: Graham's scheme had a strange asymmetry: when a word appeared # n>1 times in a single message, training added n to the word's hamcount # or spamcount, but predicting scored words only once. Tests showed # that adding only 1 in training, or scoring more than once when # predicting, hurt under the Graham scheme. # This isn't so under Robinson's scheme, though: results improve # if training also counts a word only once. The mean ham score decreases # significantly and consistently, ham score variance decreases likewise, # mean spam score decreases (but less than mean ham score, so the spread # increases), and spam score variance increases. # I (Tim) speculate that adding n times under the Graham scheme helped # because it acted against the various ham biases, giving frequently # repeated spam words (like "Viagra") a quick ramp-up in spamprob; else, # adding only once in training, a word like that was simply ignored until # it appeared in 5 distinct training spams. Without the ham-favoring # biases, though, and never ignoring words, counting n times introduces # a subtle and unhelpful bias. # There does appear to be some useful info in how many times a word # appears in a msg, but distorting spamprob doesn't appear a correct way # to exploit it. def _add_msg(self, wordstream, is_spam): self.probcache = {} # nuke the prob cache if is_spam: self.nspam += 1 else: self.nham += 1 for word in set(wordstream): record = self._wordinfoget(word) if record is None: record = self.WordInfoClass() if is_spam: record.spamcount += 1 else: record.hamcount += 1 self._wordinfoset(word, record) self._post_training() def _remove_msg(self, wordstream, is_spam): self.probcache = {} # nuke the prob cache if is_spam: if self.nspam <= 0: raise ValueError("spam count would go negative!") self.nspam -= 1 else: if self.nham <= 0: raise ValueError("non-spam count would go negative!") self.nham -= 1 for word in set(wordstream): record = self._wordinfoget(word) if record is not None: if is_spam: if record.spamcount > 0: record.spamcount -= 1 else: if record.hamcount > 0: record.hamcount -= 1 if record.hamcount == 0 == record.spamcount: self._wordinfodel(word) else: self._wordinfoset(word, record) self._post_training() def _post_training(self): """This is called after training on a wordstream. Subclasses might want to ensure that their databases are in a consistent state at this point. Introduced to fix bug #797890.""" pass # Return list of (prob, word, record) triples, sorted by increasing # prob. "word" is a token from wordstream; "prob" is its spamprob (a # float in 0.0 through 1.0); and "record" is word's associated # WordInfo record if word is in the training database, or None if it's # not. No more than max_discriminators items are returned, and have # the strongest (farthest from 0.5) spamprobs of all tokens in wordstream. # Tokens with spamprobs less than minimum_prob_strength away from 0.5 # aren't returned. def _getclues(self, wordstream): mindist = options["Classifier", "minimum_prob_strength"] if options["Classifier", "use_bigrams"]: # This scheme mixes single tokens with pairs of adjacent tokens. # wordstream is "tiled" into non-overlapping unigrams and # bigrams. Non-overlap is important to prevent a single original # token from contributing to more than one spamprob returned # (systematic correlation probably isn't a good thing). # First fill list raw with # (distance, prob, word, record), indices # pairs, one for each unigram and bigram in wordstream. # indices is a tuple containing the indices (0-based relative to # the start of wordstream) of the tokens that went into word. # indices is a 1-tuple for an original token, and a 2-tuple for # a synthesized bigram token. The indices are needed to detect # overlap later. raw = [] push = raw.append pair = None # Keep track of which tokens we've already seen. # Don't use a set here! This is an innermost loop, so speed is # important here (direct dict fiddling is much quicker than # invoking Python-level set methods; in Python 2.4 that will # change). seen = {pair: 1} # so the bigram token is skipped on 1st loop trip for i, token in enumerate(wordstream): if i: # not the 1st loop trip, so there is a preceding token # This string interpolation must match the one in # _enhance_wordstream(). pair = "bi:%s %s" % (last_token, token) last_token = token for clue, indices in (token, (i,)), (pair, (i-1, i)): if clue not in seen: # as always, skip duplicates seen[clue] = 1 tup = self._worddistanceget(clue) if tup[0] >= mindist: push((tup, indices)) # Sort raw, strongest to weakest spamprob. raw.sort() raw.reverse() # Fill clues with the strongest non-overlapping clues. clues = [] push = clues.append # Keep track of which indices have already contributed to a # clue in clues. seen = {} for tup, indices in raw: overlap = [i for i in indices if i in seen] if not overlap: # no overlap with anything already in clues for i in indices: seen[i] = 1 push(tup) # Leave sorted from smallest to largest spamprob. clues.reverse() else: # The all-unigram scheme just scores the tokens as-is. A set() # is used to weed out duplicates at high speed. clues = [] push = clues.append for word in set(wordstream): tup = self._worddistanceget(word) if tup[0] >= mindist: push(tup) clues.sort() if len(clues) > options["Classifier", "max_discriminators"]: del clues[0 : -options["Classifier", "max_discriminators"]] # Return (prob, word, record). return [t[1:] for t in clues] def _worddistanceget(self, word): record = self._wordinfoget(word) if record is None: prob = options["Classifier", "unknown_word_prob"] else: prob = self.probability(record) distance = abs(prob - 0.5) return distance, prob, word, record def _wordinfoget(self, word): return self.wordinfo.get(word) def _wordinfoset(self, word, record): self.wordinfo[word] = record def _wordinfodel(self, word): del self.wordinfo[word] def _enhance_wordstream(self, wordstream): """Add bigrams to the wordstream. For example, a b c -> a b "a b" c "b c" Note that these are *token* bigrams, and not *word* bigrams - i.e. 'synthetic' tokens get bigram'ed, too. The bigram token is simply "bi:unigram1 unigram2" - a space should be sufficient as a separator, since spaces aren't in any other tokens, apart from 'synthetic' ones. The "bi:" prefix is added to avoid conflict with tokens we generate (like "subject: word", which could be "word" in a subject, or a bigram of "subject:" and "word"). If the "Classifier":"use_bigrams" option is removed, this function can be removed, too. """ last = None for token in wordstream: yield token if last: # This string interpolation must match the one in # _getclues(). yield "bi:%s %s" % (last, token) last = token def _generate_slurp(self): # We don't want to do this recursively and check URLs # on webpages, so we have this little cheat. if not hasattr(self, "setup_done"): self.setup() self.setup_done = True if not hasattr(self, "do_slurp") or self.do_slurp: if slurp_wordstream: self.do_slurp = False tokens = self.slurp(*slurp_wordstream) self.do_slurp = True self._save_caches() return tokens return [] def setup(self): # Can't import this at the top because it's circular. # XXX Someone smarter than me, please figure out the right # XXX way to do this. from spambayes.FileCorpus import ExpiryFileCorpus, FileMessageFactory username = options["globals", "proxy_username"] password = options["globals", "proxy_password"] server = options["globals", "proxy_server"] if server.find(":") != -1: server, port = server.split(':', 1) else: port = 8080 if server: # Build a new opener that uses a proxy requiring authorization proxy_support = urllib2.ProxyHandler({"http" : \ "http://%s:%s@%s:%d" % \ (username, password, server, port)}) opener = urllib2.build_opener(proxy_support, urllib2.HTTPHandler) else: # Build a new opener without any proxy information. opener = urllib2.build_opener(urllib2.HTTPHandler) # Install it urllib2.install_opener(opener) # Setup the cache for retrieved urls age = options["URLRetriever", "x-cache_expiry_days"]*24*60*60 dir = options["URLRetriever", "x-cache_directory"] if not os.path.exists(dir): # Create the directory. if options["globals", "verbose"]: print >> sys.stderr, "Creating URL cache directory" os.makedirs(dir) self.urlCorpus = ExpiryFileCorpus(age, FileMessageFactory(), dir, cacheSize=20) # Kill any old information in the cache self.urlCorpus.removeExpiredMessages() # Setup caches for unretrievable urls self.bad_url_cache_name = os.path.join(dir, "bad_urls.pck") self.http_error_cache_name = os.path.join(dir, "http_error_urls.pck") if os.path.exists(self.bad_url_cache_name): try: self.bad_urls = pickle_read(self.bad_url_cache_name) except (IOError, ValueError): # Something went wrong loading it (bad pickle, # probably). Start afresh. if options["globals", "verbose"]: print >> sys.stderr, "Bad URL pickle, using new." self.bad_urls = {"url:non_resolving": (), "url:non_html": (), "url:unknown_error": ()} else: if options["globals", "verbose"]: print "URL caches don't exist: creating" self.bad_urls = {"url:non_resolving": (), "url:non_html": (), "url:unknown_error": ()} if os.path.exists(self.http_error_cache_name): try: self.http_error_urls = pickle_read(self.http_error_cache_name) except IOError, ValueError: # Something went wrong loading it (bad pickle, # probably). Start afresh. if options["globals", "verbose"]: print >> sys.stderr, "Bad HHTP error pickle, using new." self.http_error_urls = {} else: self.http_error_urls = {} def _save_caches(self): # XXX Note that these caches are never refreshed, which might not # XXX be a good thing long-term (if a previously invalid URL # XXX becomes valid, for example). for name, data in [(self.bad_url_cache_name, self.bad_urls), (self.http_error_cache_name, self.http_error_urls),]: pickle_write(name, data) def slurp(self, proto, url): # We generate these tokens: # url:non_resolving # url:non_html # url:http_XXX (for each type of http error encounted, # for example 404, 403, ...) # And tokenise the received page (but we do not slurp this). # Actually, the special url: tokens barely showed up in my testing, # although I would have thought that they would more - this might # be due to an error, although they do turn up on occasion. In # any case, we have to do the test, so generating an extra token # doesn't cost us anything apart from another entry in the db, and # it's only two entries, plus one for each type of http error # encountered, so it's pretty neglible. # If there is no content in the URL, then just return immediately. # "http://)" will trigger this. if not url: return ["url:non_resolving"] from spambayes.tokenizer import Tokenizer if options["URLRetriever", "x-only_slurp_base"]: url = self._base_url(url) # Check the unretrievable caches for err in self.bad_urls.keys(): if url in self.bad_urls[err]: return [err] if self.http_error_urls.has_key(url): return self.http_error_urls[url] # We check if the url will resolve first mo = DOMAIN_AND_PORT_RE.match(url) domain = mo.group(1) if mo.group(3) is None: port = 80 else: port = mo.group(3) try: _unused = socket.getaddrinfo(domain, port) except socket.error: self.bad_urls["url:non_resolving"] += (url,) return ["url:non_resolving"] # If the message is in our cache, then we can just skip over # retrieving it from the network, and get it from there, instead. url_key = URL_KEY_RE.sub('_', url) cached_message = self.urlCorpus.get(url_key) if cached_message is None: # We're going to ignore everything that isn't text/html, # so we might as well not bother retrieving anything with # these extensions. parts = url.split('.') if parts[-1] in ('jpg', 'gif', 'png', 'css', 'js'): self.bad_urls["url:non_html"] += (url,) return ["url:non_html"] # Waiting for the default timeout period slows everything # down far too much, so try and reduce it for just this # call (this will only work with Python 2.3 and above). try: timeout = socket.getdefaulttimeout() socket.setdefaulttimeout(5) except AttributeError: # Probably Python 2.2. pass try: if options["globals", "verbose"]: print >> sys.stderr, "Slurping", url f = urllib2.urlopen("%s://%s" % (proto, url)) except (urllib2.URLError, socket.error), details: mo = HTTP_ERROR_RE.match(str(details)) if mo: self.http_error_urls[url] = "url:http_" + mo.group(1) return ["url:http_" + mo.group(1)] self.bad_urls["url:unknown_error"] += (url,) return ["url:unknown_error"] # Restore the timeout try: socket.setdefaulttimeout(timeout) except AttributeError: # Probably Python 2.2. pass try: # Anything that isn't text/html is ignored content_type = f.info().get('content-type') if content_type is None or \ not content_type.startswith("text/html"): self.bad_urls["url:non_html"] += (url,) return ["url:non_html"] page = f.read() headers = str(f.info()) f.close() except socket.error: # This is probably a temporary error, like a timeout. # For now, just bail out. return [] fake_message_string = headers + "\r\n" + page # Retrieving the same messages over and over again will tire # us out, so we store them in our own wee cache. message = self.urlCorpus.makeMessage(url_key, fake_message_string) self.urlCorpus.addMessage(message) else: fake_message_string = cached_message.as_string() msg = message_from_string(fake_message_string) # We don't want to do full header tokenising, as this is # optimised for messages, not webpages, so we just do the # basic stuff. bht = options["Tokenizer", "basic_header_tokenize"] bhto = options["Tokenizer", "basic_header_tokenize_only"] options["Tokenizer", "basic_header_tokenize"] = True options["Tokenizer", "basic_header_tokenize_only"] = True tokens = Tokenizer().tokenize(msg) pf = options["URLRetriever", "x-web_prefix"] tokens = ["%s%s" % (pf, tok) for tok in tokens] # Undo the changes options["Tokenizer", "basic_header_tokenize"] = bht options["Tokenizer", "basic_header_tokenize_only"] = bhto return tokens def _base_url(self, url): # To try and speed things up, and to avoid following # unique URLS, we convert the URL to as basic a form # as we can - so http://www.massey.ac.nz/~tameyer/index.html?you=me # would become http://massey.ac.nz and http://id.example.com # would become http://example.com url += '/' domain = url.split('/', 1)[0] parts = domain.split('.') if len(parts) > 2: base_domain = parts[-2] + '.' + parts[-1] if len(parts[-1]) < 3: base_domain = parts[-3] + '.' + base_domain else: base_domain = domain return base_domain def _add_slurped(self, wordstream): """Add tokens generated by 'slurping' (i.e. tokenizing the text at the web pages pointed to by URLs in messages) to the wordstream.""" for token in wordstream: yield token slurped_tokens = self._generate_slurp() for token in slurped_tokens: yield token def _wordinfokeys(self): return self.wordinfo.keys() Bayes = Classifier spambayes-1.1a6/spambayes/compatcsv.py0000664000076500000240000000653210646440130020222 0ustar skipstaff00000000000000#!/usr/bin/env python """Implement just enough of a csv parser to support sb_dbexpimp.py's needs.""" import sys import re if sys.platform == "windows": EOL = "\r\n" elif sys.platform == "mac": EOL = "\r" else: EOL = "\n" class reader: def __init__(self, fp): self.fp = fp def __iter__(self): return self def _readline(self): line = self.fp.readline() # strip any EOL detritus while line[-1:] in ("\r", "\n"): line = line[:-1] return line def next(self): line = self._readline() if not line: raise StopIteration return self.parse_line(line) def parse_line(self, line): """parse the line. very simple assumptions: * separator is a comma * fields are only quoted with quotation marks and only quoted if the field contains a comma or a quotation mark * embedded quotation marks are doubled """ result = [] while line: # quoted field if line[0] == '"': line = line[1:] field = [] while True: if line[0:2] == '""': field.append('"') line = line[2:] elif line[0] == '"': # end of field - skip quote and possible comma line = line[1:] if line[0:1] == ',': line = line[1:] break else: field.append(line[0]) line = line[1:] # ran out of line before field if not line: field.append("\n") line = self._readline() if not line: raise IOError, "end-of-file during parsing" else: # unquoted field field = [] while line: if line[0] == ',': # end of field line = line[1:] break else: field.append(line[0]) line = line[1:] result.append("".join(field)) return result class writer: def __init__(self, fp): self.fp = fp def writerow(self, row): result = [] for item in row: if isinstance(item, unicode): item = item.encode("utf-8") else: item = str(item) if re.search('["\n,]', item) is not None: item = '"%s"' % item.replace('"', '""') result.append(item) result = ",".join(result) self.fp.write(result+EOL) if __name__ == "__main__": import unittest import StringIO class TestCase(unittest.TestCase): def test_reader(self): f = StringIO.StringIO('''\ """rare""",1,0 "beginning; end=""itinhh.txt""",1,0 ''') f.seek(0) rdr = reader(f) self.assertEqual(rdr.next(), ['"rare"', '1', '0']) self.assertEqual(rdr.next(), ['beginning;\n\tend="itinhh.txt"','1', '0']) self.assertRaises(StopIteration, rdr.next) unittest.main() spambayes-1.1a6/spambayes/compatheapq.py0000664000076500000240000002565510646440130020534 0ustar skipstaff00000000000000# -*- coding: Latin-1 -*- """Heap queue algorithm (a.k.a. priority queue). Heaps are arrays for which a[k] <= a[2*k+1] and a[k] <= a[2*k+2] for all k, counting elements from 0. For the sake of comparison, non-existing elements are considered to be infinite. The interesting property of a heap is that a[0] is always its smallest element. Usage: heap = [] # creates an empty heap heappush(heap, item) # pushes a new item on the heap item = heappop(heap) # pops the smallest item from the heap item = heap[0] # smallest item on the heap without popping it heapify(x) # transforms list into a heap, in-place, in linear time item = heapreplace(heap, item) # pops and returns smallest item, and adds # new item; the heap size is unchanged Our API differs from textbook heap algorithms as follows: - We use 0-based indexing. This makes the relationship between the index for a node and the indexes for its children slightly less obvious, but is more suitable since Python uses 0-based indexing. - Our heappop() method returns the smallest item, not the largest. These two make it possible to view the heap as a regular Python list without surprises: heap[0] is the smallest item, and heap.sort() maintains the heap invariant! """ # Original code by Kevin O'Connor, augmented by Tim Peters __about__ = """Heap queues [explanation by Franois Pinard] Heaps are arrays for which a[k] <= a[2*k+1] and a[k] <= a[2*k+2] for all k, counting elements from 0. For the sake of comparison, non-existing elements are considered to be infinite. The interesting property of a heap is that a[0] is always its smallest element. The strange invariant above is meant to be an efficient memory representation for a tournament. The numbers below are `k', not a[k]: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 In the tree above, each cell `k' is topping `2*k+1' and `2*k+2'. In an usual binary tournament we see in sports, each cell is the winner over the two cells it tops, and we can trace the winner down the tree to see all opponents s/he had. However, in many computer applications of such tournaments, we do not need to trace the history of a winner. To be more memory efficient, when a winner is promoted, we try to replace it by something else at a lower level, and the rule becomes that a cell and the two cells it tops contain three different items, but the top cell "wins" over the two topped cells. If this heap invariant is protected at all time, index 0 is clearly the overall winner. The simplest algorithmic way to remove it and find the "next" winner is to move some loser (let's say cell 30 in the diagram above) into the 0 position, and then percolate this new 0 down the tree, exchanging values, until the invariant is re-established. This is clearly logarithmic on the total number of items in the tree. By iterating over all items, you get an O(n ln n) sort. A nice feature of this sort is that you can efficiently insert new items while the sort is going on, provided that the inserted items are not "better" than the last 0'th element you extracted. This is especially useful in simulation contexts, where the tree holds all incoming events, and the "win" condition means the smallest scheduled time. When an event schedule other events for execution, they are scheduled into the future, so they can easily go into the heap. So, a heap is a good structure for implementing schedulers (this is what I used for my MIDI sequencer :-). Various structures for implementing schedulers have been extensively studied, and heaps are good for this, as they are reasonably speedy, the speed is almost constant, and the worst case is not much different than the average case. However, there are other representations which are more efficient overall, yet the worst cases might be terrible. Heaps are also very useful in big disk sorts. You most probably all know that a big sort implies producing "runs" (which are pre-sorted sequences, which size is usually related to the amount of CPU memory), followed by a merging passes for these runs, which merging is often very cleverly organised[1]. It is very important that the initial sort produces the longest runs possible. Tournaments are a good way to that. If, using all the memory available to hold a tournament, you replace and percolate items that happen to fit the current run, you'll produce runs which are twice the size of the memory for random input, and much better for input fuzzily ordered. Moreover, if you output the 0'th item on disk and get an input which may not fit in the current tournament (because the value "wins" over the last output value), it cannot fit in the heap, so the size of the heap decreases. The freed memory could be cleverly reused immediately for progressively building a second heap, which grows at exactly the same rate the first heap is melting. When the first heap completely vanishes, you switch heaps and start a new run. Clever and quite effective! In a word, heaps are useful memory structures to know. I use them in a few applications, and I think it is good to keep a `heap' module around. :-) -------------------- [1] The disk balancing algorithms which are current, nowadays, are more annoying than clever, and this is a consequence of the seeking capabilities of the disks. On devices which cannot seek, like big tape drives, the story was quite different, and one had to be very clever to ensure (far in advance) that each tape movement will be the most effective possible (that is, will best participate at "progressing" the merge). Some tapes were even able to read backwards, and this was also used to avoid the rewinding time. Believe me, real good tape sorts were quite spectacular to watch! From all times, sorting has always been a Great Art! :-) """ def heappush(heap, item): """Push item onto heap, maintaining the heap invariant.""" heap.append(item) _siftdown(heap, 0, len(heap)-1) def heappop(heap): """Pop the smallest item off the heap, maintaining the heap invariant.""" lastelt = heap.pop() # raises appropriate IndexError if heap is empty if heap: returnitem = heap[0] heap[0] = lastelt _siftup(heap, 0) else: returnitem = lastelt return returnitem def heapreplace(heap, item): """Pop and return the current smallest value, and add the new item. This is more efficient than heappop() followed by heappush(), and can be more appropriate when using a fixed-size heap. Note that the value returned may be larger than item! That constrains reasonable uses of this routine. """ returnitem = heap[0] # raises appropriate IndexError if heap is empty heap[0] = item _siftup(heap, 0) return returnitem def heapify(x): """Transform list into a heap, in-place, in O(len(heap)) time.""" n = len(x) # Transform bottom-up. The largest index there's any point to looking at # is the largest with a child index in-range, so must have 2*i + 1 < n, # or i < (n-1)/2. If n is even = 2*j, this is (2*j-1)/2 = j-1/2 so # j-1 is the largest, which is n//2 - 1. If n is odd = 2*j+1, this is # (2*j+1-1)/2 = j so j-1 is the largest, and that's again n//2-1. for i in xrange(n//2 - 1, -1, -1): _siftup(x, i) # 'heap' is a heap at all indices >= startpos, except possibly for pos. pos # is the index of a leaf with a possibly out-of-order value. Restore the # heap invariant. def _siftdown(heap, startpos, pos): newitem = heap[pos] # Follow the path to the root, moving parents down until finding a place # newitem fits. while pos > startpos: parentpos = (pos - 1) >> 1 parent = heap[parentpos] if parent <= newitem: break heap[pos] = parent pos = parentpos heap[pos] = newitem # The child indices of heap index pos are already heaps, and we want to make # a heap at index pos too. We do this by bubbling the smaller child of # pos up (and so on with that child's children, etc) until hitting a leaf, # then using _siftdown to move the oddball originally at index pos into place. # # We *could* break out of the loop as soon as we find a pos where newitem <= # both its children, but turns out that's not a good idea, and despite that # many books write the algorithm that way. During a heap pop, the last array # element is sifted in, and that tends to be large, so that comparing it # against values starting from the root usually doesn't pay (= usually doesn't # get us out of the loop early). See Knuth, Volume 3, where this is # explained and quantified in an exercise. # # Cutting the # of comparisons is important, since these routines have no # way to extract "the priority" from an array element, so that intelligence # is likely to be hiding in custom __cmp__ methods, or in array elements # storing (priority, record) tuples. Comparisons are thus potentially # expensive. # # On random arrays of length 1000, making this change cut the number of # comparisons made by heapify() a little, and those made by exhaustive # heappop() a lot, in accord with theory. Here are typical results from 3 # runs (3 just to demonstrate how small the variance is): # # Compares needed by heapify Compares needed by 1000 heapppops # -------------------------- --------------------------------- # 1837 cut to 1663 14996 cut to 8680 # 1855 cut to 1659 14966 cut to 8678 # 1847 cut to 1660 15024 cut to 8703 # # Building the heap by using heappush() 1000 times instead required # 2198, 2148, and 2219 compares: heapify() is more efficient, when # you can use it. # # The total compares needed by list.sort() on the same lists were 8627, # 8627, and 8632 (this should be compared to the sum of heapify() and # heappop() compares): list.sort() is (unsurprisingly!) more efficient # for sorting. def _siftup(heap, pos): endpos = len(heap) startpos = pos newitem = heap[pos] # Bubble up the smaller child until hitting a leaf. childpos = 2*pos + 1 # leftmost child position while childpos < endpos: # Set childpos to index of smaller child. rightpos = childpos + 1 if rightpos < endpos and heap[rightpos] <= heap[childpos]: childpos = rightpos # Move the smaller child up. heap[pos] = heap[childpos] pos = childpos childpos = 2*pos + 1 # The leaf at pos is empty now. Put newitem there, and and bubble it up # to its final resting place (by sifting its parents down). heap[pos] = newitem _siftdown(heap, startpos, pos) if __name__ == "__main__": # Simple sanity test heap = [] data = [1, 3, 5, 7, 9, 2, 4, 6, 8, 0] for item in data: heappush(heap, item) sort = [] while heap: sort.append(heappop(heap)) print sort spambayes-1.1a6/spambayes/compatsets.py0000664000076500000240000003743611116605704020417 0ustar skipstaff00000000000000"""Classes to represent arbitrary sets (including sets of sets). This module implements sets using dictionaries whose values are ignored. The usual operations (union, intersection, deletion, etc.) are provided as both methods and operators. Important: sets are not sequences! While they support 'x in s', 'len(s)', and 'for x in s', none of those operations are unique for sequences; for example, mappings support all three as well. The characteristic operation for sequences is subscripting with small integers: s[i], for i in range(len(s)). Sets don't support subscripting at all. Also, sequences allow multiple occurrences and their elements have a definite order; sets on the other hand don't record multiple occurrences and don't remember the order of element insertion (which is why they don't support s[i]). The following classes are provided: BaseSet -- All the operations common to both mutable and immutable sets. This is an abstract class, not meant to be directly instantiated. Set -- Mutable sets, subclass of BaseSet; not hashable. ImmutableSet -- Immutable sets, subclass of BaseSet; hashable. An iterable argument is mandatory to create an ImmutableSet. _TemporarilyImmutableSet -- Not a subclass of BaseSet: just a wrapper around a Set, hashable, giving the same hash value as the immutable set equivalent would have. Do not use this class directly. Only hashable objects can be added to a Set. In particular, you cannot really add a Set as an element to another Set; if you try, what is actually added is an ImmutableSet built from it (it compares equal to the one you tried adding). When you ask if `x in y' where x is a Set and y is a Set or ImmutableSet, x is wrapped into a _TemporarilyImmutableSet z, and what's tested is actually `z in y'. """ # Code history: # # - Greg V. Wilson wrote the first version, using a different approach # to the mutable/immutable problem, and inheriting from dict. # # - Alex Martelli modified Greg's version to implement the current # Set/ImmutableSet approach, and make the data an attribute. # # - Guido van Rossum rewrote much of the code, made some API changes, # and cleaned up the docstrings. # # - Raymond Hettinger added a number of speedups and other # improvements. __all__ = ['BaseSet', 'Set', 'ImmutableSet'] class BaseSet(object): """Common base class for mutable and immutable sets.""" __slots__ = ['_data'] # Constructor def __init__(self): """This is an abstract class.""" # Don't call this from a concrete subclass! if self.__class__ is BaseSet: raise TypeError, ("BaseSet is an abstract class. " "Use Set or ImmutableSet.") # Standard protocols: __len__, __repr__, __str__, __iter__ def __len__(self): """Return the number of elements of a set.""" return len(self._data) def __repr__(self): """Return string representation of a set. This looks like 'Set([])'. """ return self._repr() # __str__ is the same as __repr__ __str__ = __repr__ def _repr(self, sorted=False): elements = self._data.keys() if sorted: elements.sort() return '%s(%r)' % (self.__class__.__name__, elements) def __iter__(self): """Return an iterator over the elements or a set. This is the keys iterator for the underlying dict. """ return self._data.iterkeys() # Equality comparisons using the underlying dicts def __eq__(self, other): self._binary_sanity_check(other) return self._data == other._data def __ne__(self, other): self._binary_sanity_check(other) return self._data != other._data # Copying operations def copy(self): """Return a shallow copy of a set.""" result = self.__class__() result._data.update(self._data) return result __copy__ = copy # For the copy module def __deepcopy__(self, memo): """Return a deep copy of a set; used by copy module.""" # This pre-creates the result and inserts it in the memo # early, in case the deep copy recurses into another reference # to this same set. A set can't be an element of itself, but # it can certainly contain an object that has a reference to # itself. from copy import deepcopy result = self.__class__() memo[id(self)] = result data = result._data value = True for elt in self: data[deepcopy(elt, memo)] = value return result # Standard set operations: union, intersection, both differences. # Each has an operator version (e.g. __or__, invoked with |) and a # method version (e.g. union). # Subtle: Each pair requires distinct code so that the outcome is # correct when the type of other isn't suitable. For example, if # we did "union = __or__" instead, then Set().union(3) would return # NotImplemented instead of raising TypeError (albeit that *why* it # raises TypeError as-is is also a bit subtle). def __or__(self, other): """Return the union of two sets as a new set. (I.e. all elements that are in either set.) """ if not isinstance(other, BaseSet): return NotImplemented result = self.__class__() result._data = self._data.copy() result._data.update(other._data) return result def union(self, other): """Return the union of two sets as a new set. (I.e. all elements that are in either set.) """ return self | other def __and__(self, other): """Return the intersection of two sets as a new set. (I.e. all elements that are in both sets.) """ if not isinstance(other, BaseSet): return NotImplemented if len(self) <= len(other): little, big = self, other else: little, big = other, self common = filter(big._data.has_key, little._data.iterkeys()) return self.__class__(common) def intersection(self, other): """Return the intersection of two sets as a new set. (I.e. all elements that are in both sets.) """ return self & other def __xor__(self, other): """Return the symmetric difference of two sets as a new set. (I.e. all elements that are in exactly one of the sets.) """ if not isinstance(other, BaseSet): return NotImplemented result = self.__class__() data = result._data value = True selfdata = self._data otherdata = other._data for elt in selfdata: if elt not in otherdata: data[elt] = value for elt in otherdata: if elt not in selfdata: data[elt] = value return result def symmetric_difference(self, other): """Return the symmetric difference of two sets as a new set. (I.e. all elements that are in exactly one of the sets.) """ return self ^ other def __sub__(self, other): """Return the difference of two sets as a new Set. (I.e. all elements that are in this set and not in the other.) """ if not isinstance(other, BaseSet): return NotImplemented result = self.__class__() data = result._data otherdata = other._data value = True for elt in self: if elt not in otherdata: data[elt] = value return result def difference(self, other): """Return the difference of two sets as a new Set. (I.e. all elements that are in this set and not in the other.) """ return self - other # Membership test def __contains__(self, element): """Report whether an element is a member of a set. (Called in response to the expression `element in self'.) """ try: return element in self._data except TypeError: transform = getattr(element, "_as_temporarily_immutable", None) if transform is None: raise # re-raise the TypeError exception we caught return transform() in self._data # Subset and superset test def issubset(self, other): """Report whether another set contains this set.""" self._binary_sanity_check(other) if len(self) > len(other): # Fast check for obvious cases return False otherdata = other._data for elt in self: if elt not in otherdata: return False return True def issuperset(self, other): """Report whether this set contains another set.""" self._binary_sanity_check(other) if len(self) < len(other): # Fast check for obvious cases return False selfdata = self._data for elt in other: if elt not in selfdata: return False return True # Inequality comparisons using the is-subset relation. __le__ = issubset __ge__ = issuperset def __lt__(self, other): self._binary_sanity_check(other) return len(self) < len(other) and self.issubset(other) def __gt__(self, other): self._binary_sanity_check(other) return len(self) > len(other) and self.issuperset(other) # Assorted helpers def _binary_sanity_check(self, other): # Check that the other argument to a binary operation is also # a set, raising a TypeError otherwise. if not isinstance(other, BaseSet): raise TypeError, "Binary operation only permitted between sets" def _compute_hash(self): # Calculate hash code for a set by xor'ing the hash codes of # the elements. This ensures that the hash code does not depend # on the order in which elements are added to the set. This is # not called __hash__ because a BaseSet should not be hashable; # only an ImmutableSet is hashable. result = 0 for elt in self: result ^= hash(elt) return result def _update(self, iterable): # The main loop for update() and the subclass __init__() methods. data = self._data # Use the fast update() method when a dictionary is available. if isinstance(iterable, BaseSet): data.update(iterable._data) return if isinstance(iterable, dict): data.update(iterable) return value = True it = iter(iterable) while True: try: for element in it: data[element] = value return except TypeError: transform = getattr(element, "_as_immutable", None) if transform is None: raise # re-raise the TypeError exception we caught data[transform()] = value class ImmutableSet(BaseSet): """Immutable set class.""" __slots__ = ['_hashcode'] # BaseSet + hashing def __init__(self, iterable=None): """Construct an immutable set from an optional iterable.""" self._hashcode = None self._data = {} if iterable is not None: self._update(iterable) def __hash__(self): if self._hashcode is None: self._hashcode = self._compute_hash() return self._hashcode class Set(BaseSet): """ Mutable set class.""" __slots__ = [] # BaseSet + operations requiring mutability; no hashing def __init__(self, iterable=None): """Construct a set from an optional iterable.""" self._data = {} if iterable is not None: self._update(iterable) def __hash__(self): """A Set cannot be hashed.""" # We inherit object.__hash__, so we must deny this explicitly raise TypeError, "Can't hash a Set, only an ImmutableSet." # In-place union, intersection, differences. # Subtle: The xyz_update() functions deliberately return None, # as do all mutating operations on built-in container types. # The __xyz__ spellings have to return self, though. def __ior__(self, other): """Update a set with the union of itself and another.""" self._binary_sanity_check(other) self._data.update(other._data) return self def union_update(self, other): """Update a set with the union of itself and another.""" self |= other def __iand__(self, other): """Update a set with the intersection of itself and another.""" self._binary_sanity_check(other) self._data = (self & other)._data return self def intersection_update(self, other): """Update a set with the intersection of itself and another.""" self &= other def __ixor__(self, other): """Update a set with the symmetric difference of itself and another.""" self._binary_sanity_check(other) data = self._data value = True for elt in other: if elt in data: del data[elt] else: data[elt] = value return self def symmetric_difference_update(self, other): """Update a set with the symmetric difference of itself and another.""" self ^= other def __isub__(self, other): """Remove all elements of another set from this set.""" self._binary_sanity_check(other) data = self._data for elt in other: if elt in data: del data[elt] return self def difference_update(self, other): """Remove all elements of another set from this set.""" self -= other # Python dict-like mass mutations: update, clear def update(self, iterable): """Add all values from an iterable (such as a list or file).""" self._update(iterable) def clear(self): """Remove all elements from this set.""" self._data.clear() # Single-element mutations: add, remove, discard def add(self, element): """Add an element to a set. This has no effect if the element is already present. """ try: self._data[element] = True except TypeError: transform = getattr(element, "_as_immutable", None) if transform is None: raise # re-raise the TypeError exception we caught self._data[transform()] = True def remove(self, element): """Remove an element from a set; it must be a member. If the element is not a member, raise a KeyError. """ try: del self._data[element] except TypeError: transform = getattr(element, "_as_temporarily_immutable", None) if transform is None: raise # re-raise the TypeError exception we caught del self._data[transform()] def discard(self, element): """Remove an element from a set if it is a member. If the element is not a member, do nothing. """ try: self.remove(element) except KeyError: pass def pop(self): """Remove and return an arbitrary set element.""" return self._data.popitem()[0] def _as_immutable(self): # Return a copy of self as an immutable set return ImmutableSet(self) def _as_temporarily_immutable(self): # Return self wrapped in a temporarily immutable set return _TemporarilyImmutableSet(self) class _TemporarilyImmutableSet(BaseSet): # Wrap a mutable set as if it was temporarily immutable. # This only supplies hashing and equality comparisons. def __init__(self, set): self._set = set self._data = set._data # Needed by ImmutableSet.__eq__() def __hash__(self): return self._set._compute_hash() spambayes-1.1a6/spambayes/core_resources/0000775000076500000240000000000011355064627020700 5ustar skipstaff00000000000000spambayes-1.1a6/spambayes/core_resources/__init__.py0000664000076500000240000000213211137564467023014 0ustar skipstaff00000000000000"""Design-time __init__.py for resourcepackage This is the scanning version of __init__.py for your resource modules. You replace it with a blank or doc-only init when ready to release. """ import os if os.path.splitext(os.path.basename( __file__ ))[0] == "__init__": try: from resourcepackage import package, defaultgenerators generators = defaultgenerators.generators.copy() ### CUSTOMISATION POINT ## import specialised generators here, such as for wxPython #from resourcepackage import wxgenerators #generators.update( wxgenerators.generators ) except ImportError: pass else: package = package.Package( packageName = __name__, directory = os.path.dirname( os.path.abspath(__file__) ), generators = generators, ) package.scan( ### CUSTOMISATION POINT ## force true -> always re-loads from external files, otherwise ## only reloads if the file is newer than the generated .py file. # force = 1, ) spambayes-1.1a6/spambayes/core_resources/classify_gif.py0000664000076500000240000000665510646440122023716 0ustar skipstaff00000000000000# -*- coding: ISO-8859-1 -*- """Resource classify_gif (from file classify.gif)""" # written by resourcepackage: (1, 0, 0) source = 'classify.gif' package = 'spambayes.resources' data = "GIF89a(\000(\000\000\000\000\004\010\014\020\024\030\034 $(,048<\ @DHLPTX\\`dhlptx|\ \ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000,\000\000\000\000(\000(\000\000\010\000\010\034H\010\023*\\ȰÇ\020#JHŋ\022o\000\006\030\\(q\003\017\024\015>\ \\\001\037\024]$@\004\023+\003<\030\021!\004-\"\001>X\000\004\002\005:n`\011 \017=\036\000\010@`J\ \003\034t< \006;\030fݺb ́\034TBEB\0134{,1\005A\0072\006ޠ\012\011\011\004\002\020G\014\005\005x\020\ B\007\020\012*\017 @(\022\"X \000 \037\005\\\010\022:\006 \0029F\0043A\000B'\013\003\012'>\ \006!\005\027/\\\013\000U 0\020e@\002p?;]+\000\0064\004Z\000o\017:*\0053\016\020ݺ\001G\ V\004'\003\012\011@\003\010\004|u\017Q\000\022 \001a\001@@\005\002\024D'\034K.VF\034y\004H$a,\ 0(4h8+\006\004\000;" ### end spambayes-1.1a6/spambayes/core_resources/config_gif.py0000664000076500000240000000574210646440122023342 0ustar skipstaff00000000000000# -*- coding: ISO-8859-1 -*- """Resource config_gif (from file config.gif)""" # written by resourcepackage: (1, 0, 0) source = 'config.gif' package = 'spambayes.resources' data = "GIF89a(\000(\000\000\000\000\004\010\014\020\024\030\034 $(,048<\ @DHLPTX\\`dhlptx|\ \ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000,\000\000\000\000(\000(\000\000\010\000\010\034H\010\023*\\ȰÇ\020#J\010\003E\034\000\\QƆ\035=~T\030Rȃ%M\ $R%C \001Ȝr!\000$iҌ(\023'J;!\022PG\013L*ϥ='\002\011&E\034\022mjV\ ]u~\030VɪLG\035{\021*ۍ-~,)Wmԕ$˷߿\003\013\036L\001\001\000;" ### end spambayes-1.1a6/spambayes/core_resources/helmet_gif.py0000664000076500000240000000715410646440122023352 0ustar skipstaff00000000000000# -*- coding: ISO-8859-1 -*- """Resource helmet_gif (from file helmet.gif)""" # written by resourcepackage: (1, 0, 0) source = 'helmet.gif' package = 'spambayes.resources' data = "GIF89a\"\000\030\000\000\000BBFUSTcZR^^^kZVoe\\kkgskcwogtf{wt~kp\ ޽Ω֯ν\ ֽ\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000!\004\001\000\000\036\000,\000\000\000\000\"\000\030\000\000\010\000=\010\034H\003ea\015\020#~`\007\034;>DXP\006\013\0362b\ ࡑI\032,|\020cI4Na\017\032/aQf΍4Zȡß\020E\016UGƍ\026K\"R\ =p\"\002\0141 z1\024̡R\011~@\002\007\026\032jsh\023V{H\001\005\005\006\006\0240 \002\012\037bth\ G\017%ꕁ\"Æ\030\023\024\014\\)P`\020\004\027?.p\007\017\014\013bC,\000Ξ\0258p\005z\ \0219u\013\007\006\002o\036 6\003\017ũsIaeD<\000\0011ZTD+T\036_P|wgfn\"+\ \006-\013ֲi\016{ \011V0j4PFn\017٬>Z^CQ\013[\020\024Mp+b&\031JC\ [>xeF{ǵHrap%\000[By~\037£\037#\015OMP \030/(9\ C always re-loads from external files, otherwise ## only reloads if the file is newer than the generated .py file. # force = 1, ) # ResourcePackage license added by Richie Hindle , # since this is "Redistribution and use in source form". Note that binary # Spambayes packages don't redistribute this file or rely on ResourcePackage; # it's only used at development time (and even developers don't need it # unless they want to change the resources). Kudos to Mike Fletcher for # ResourcePackage - excellent tool! __license__ = """ ResourcePackage License Copyright (c) 2003, Michael C. Fletcher, All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. The name of Michael C. Fletcher, or the name of any Contributor, may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS NOT FAULT TOLERANT AND SHOULD NOT BE USED IN ANY SITUATION ENDANGERING HUMAN LIFE OR PROPERTY. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ spambayes-1.1a6/spambayes/core_resources/status_gif.py0000664000076500000240000000661510646440122023420 0ustar skipstaff00000000000000# -*- coding: ISO-8859-1 -*- """Resource status_gif (from file status.gif)""" # written by resourcepackage: (1, 0, 0) source = 'status.gif' package = 'spambayes.resources' data = "GIF89a(\000(\000\000\000\000\004\010\014\020\024\030\034 $(,048<\ @DHLPTX\\`dhlptx|\ \ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000,\000\000\000\000(\000(\000\000\010\000\010\034H\010\023*\\ȰÇ\020#J$xC\004\004\001\000\004@0Ač\010c@\000@I\000\020b\ \034#\003\001\031T\001\015\025\031\006\003\005\000\004\007\010\001\000\026萨C\001\000\0055\010@\000\000\0106*X\007\ \007=\0078\011`\007I6t@@&\007\0244\030A\026[RnA\033HU2\000&\015\027\034\033\015\000\003\036Z\ A\036:?*\024\001J .E,\000@g *\000LX\000'\023\000`aF\010c#q!Is\003\015\000\006y\ \017WH𒲫.\015dԫ7w\016\035a腏#s?}93Ñ\013\"'8\026BC\030An\0200\000\030\01615\036\ W\000\030t[@w~\017`S\025H@Y\001`@\016L\001%d\031R \037d1TM9\001\022tҊ)F\ E\030iđG.h8`yd\007\012<\011?\005\015?HA4f\013\014P=t\020\ \004\006AK\013 \024\014\010\024UK?\006R\014\014t@M#,\000$\004AT\0003\000A\007 \022x0\022\010UT\001\000@\000f\000\024\ \006+\026kB\001\001\000;" ### end spambayes-1.1a6/spambayes/core_resources/ui_html.py0000664000076500000240000003116110646440122022703 0ustar skipstaff00000000000000# -*- coding: ISO-8859-1 -*- """Resource ui_html (from file ui.html)""" # written by resourcepackage: (1, 0, 0) source = 'ui.html' package = 'spambayes.core_resources' import zlib data = zlib.decompress("x=ksF+&ز!R$%N\007omYΎ}T*\032\002C\022+\020@\020~\014\006\017\ ݫ:gW\"\001LOwO\007:߿\022Wo߈?xRxѧKd8>\022׹L\ Dƣѫ\037\\K\020~Q\031+\021\027\036}&\0373|!ת\020iGߩ\\TI\014\ \036KUJ(PVEw\027ޥ\014\026\023A\012&兗\001_\032=>rU8\003\027@\000)\023qc1\ \036_^oB\036^\002<{&\037eUq\024\022.\004\002\031bQi\032b\006OGB\ \007XEE1\020\013\025ߩ2\012X|\036%b,W)pa,͗;|OQr\013W4NS1\013\ <`TsԷg35\034\020RuR|\001M8\007\"Y;O$\021T&\011M0\ ҌF\037#t9&|w@\031@a!jah\ ąH3{Fy\003w\000V\007\001\012\000\017t\005U>\021J|!az'\ >#t\002h3=\016\016C\002\006\004z\013Uc\020h3\002j:2\037S\022emkPnU\021\021V[\006=\ gA\"\025q3\0111u&\022m)FG\034!\003\014\037\022ڜ\023Y.E\021\004\0321R`\ 'a\035\0108\001\005,\005.h\026\036\002`ʡ\010A85\"+\026J=5eO(?H\025\022\015\ &T\017sVo1\034\006B\031\\\013Xźi#_\030\">ަk6\023) \026\022\037>R\ h\020Ff&JqKoBX\002\015EƉMO\013\017g\034:s\020_\004*a`io\015SO!5\032\ /(8\014q\033M,Ȯ!(\014i\003#XKG0\024\0229\005\036] jF[\033\011%#2ncxV\013nѪ+A?\ CEx\035D\0341i\010q-'ǻ\0128axL|2A\036\012L֢T\020͢G(&R\035\ {kh\036,j A\005rE.F#]FH\001\030b0\003P ۞|\012AR\032S\002\023@>\002Q&\001ڀ\037R\ R-X(!#\001ډcj|O1$\034\012A+AU@6\024nD9xl\005!\007\030\0076\031\0164\004\010\002\ b\0069ϭB\022˼\005:h+Y$fg\026\0208A^\0024\004*[mf%xot\012\022\006\ \001f9P>\0034]\034\020\000\027\"I9\\G{:\032\025s5LT9V@=\001yv\ s\036G#\004=Mк(\022+\022\003\\\002R=[a\003P\014L\007\026g\000&,,\003\022\034\000U\026<\036\ z$Ð\000(ЭX@3\010B\023\000\021\034@L9<\0204LQU\025#M,?Jd̶\037\017a\ bg\004\030\021&?BTs9\013,'ˀ\013\025%f)\007aT[X6Cwp\0328\ \020\033jO\002bJG\017l\015\\'R#nh5=XiAP\020;pX`Ua\012.O\0114hpq\007\ \014l0s;PM!\036,hE\026\003_.9xz\022|\026 !!ku?\ <\0017o\034*v-wYo~8}\021}~OZAFr`54Hgp\ ߱K\016-*S`\020~EcEB\\0:+C¨q\006\012HEB\031\012\027q\016*E\031\ \014yJLOx=\030\000X=\002\002\015 J\036\032t\010E\030CwJ\027\034b\033\006At\022q<\032\006\ ]aa\012\037e\013p:2\000\010DH\023 CU\\TA\010u5\021d\004a%\011.P\033\011Kfk,2e\022\ \031\037%L\011U,-\032Q\027\030Z*SI(\023\012rT\\\"6\006M=LC3\\0%ށ/8\ y#i\023Z H\000XՂub\034u5kV \032C\016IQk\017p졗Y*\005|Oz7\ \0127\0177\\\027\007,/F\014\000x\015Fx\024z\022\012\025\016\005x\014^?=%!`ܿ!ݱA\021Z\005\\8\030aT\0042\017\ \017S\024h@`\017I\017\006H5S3\016ȝ5T`52&g\037`@c\020 \026ǡa!\026͡\ \002\023\007ܡU\"\024\031`@8؝j\007Q\036F1\021C\004Y\036a\016j7\0053\020$\ BRAS\006GL3\023tC\032c:2\011;F\035\\\005{!\005#U\037s\007\037t\035f\ W'|\001U\010quAX\022KmC\030t\000o\\U\012`\037KV\0340\\h\ ր\023pq\017-\007\007!\030\\MYRVZ\037Rc\002/Wy䱦+md;ѵO\005\005)t8ċX4\037\\\ -vif\021@\037\025\031C+\016\003grr}>O<%`\032 Fc:%\024V\031n,\ d\035$FxIY;n76\016+Ɛ|y\025>ÚȤU\034B8\"\026Y3͠s4V\021\031\037͌\ \014\013<\023\003'n\02121@&W@H\000\000'kp\005CQ7\003\014*\031#\ LƯ*\012]\035\012\026I\007\0242*V\011\010\004\0278Ơx-xu\005AɁ\\\025\003{\025r{\013\ \0062\032JT\015,eqEF=lu^8 ~d4޳1JKA5\ `#\012|a*PL,=\016j)\\Ɉ'>l{\021M^q*`\000f(\020RK&\001\026[?\ m2\025F4sՏl!\012+_I3\0269qTɦ\035\017&\034ZA\035g\005%\0044\ *S\017Ͼ跦\005\031aݕ3G|P[\027>ӢyH\002a|'\004f\001\003\000((G\ zH;0U*+8E$tB'\032{BB\006 \012?\013M\013\007zh\025\030>\0337&!3\003\ DXf2o%\033x]ː$\030TX@A򷣀\011\000B\003x\020\033A\006\030a\006ݣ.׊\0253\ 0O(\033sY\002S3\003hi\037\000<\023De0d;ʍ\035$ 8xG\022Y\037X1U7\ oUZ\031{+a\0305M\023,N2h\004\020\007TS\007e\022X@^pF\037\016)\0036@\ xMd.#whތQM8[FGഝ0ޙ7(P\\\027tƸ\ 0\005\026d\034\021`,\027\0348Y4r.64\026^;HeD%R,q\036^쿫J}ū{\011\\\ \032n\021ab/Zŷ>#%\001\005B=O\02191I\017LzBPKcGa0\013,+ll>j\ \025\010\023;\001\0216Z\\QLQ}t\027\024]aZ,{c\000\0207b]g`XŇ:.o\ 8.9|\000_qUUvGD(,\036\022R\034t3n4ʡ!?a\027\031ƖX\ pu\002ԑ\"1z\"ӫ \033J$d&5/\0132\0027#\014̫LjKѪ]KL@Vq\020l\ ݺ\0250>\001h\000ߕך\021DDk\\ 4\004ff¢\005L\005jmkk`x}\021wp]W\ )\025\027x(\001\012p\007\000\021\"~X\015K2!tr5NJPטJu6\034{wxj\001\005\012\011\ 'Q^E\\NF6vX\001J2\002qTKX\"^\003!mӦd\022Nܪ\ )uӁ\026V\006(n\027v\014[xRsO3z*n #\ uXϩ'?\0141©zxn~O>b\015\010GMxC\007ЮS\036ቾUu[\ x)PKo uFpn||\0334~dCd2i\032žieM{>\021`SW\004\ {\\&\006gڦq⓯XP\011V\034\\fZu%\0051\015982[0\036\ -=\030=\027S,v&Sxbc\015@1a[\027\010\013ZtmBѷG[T\ >\015З\003d\024\02634\014\026F'\016͗:FN\006=6sD*ݺ>CZ;\034\036\ \023\014ဍ@ƒ\032VR-1\001hcu>N\004\003\015\036\034u\0356'FqydX7Q\ CZ\0118Tlҁx@\033C#\026\001MDl9\\\027ަ\014Ƒ\025\017{˵\035x\ }l\016qϜ)9 .\035<(i\007I2'K\033\033\007շM\005\003{A\036.ѹҤ\\9\006\004;N\013.G06p\ ڶp41ysH\012}\010+XEw7ŽCU0F\030V!\024Zv\012\025R`mw|F\ 7/qڴ2{]ߑxdT_6&>\0323EJ)c^L'\033XYjP\022ڬկ\ 00Ļ:\030v\012/\027^Pk@#Q@\007ɤ0\015\031زRLc\030\011\015O\ \\{OӮ?\022R7`d\032\012Mn=Vׂ/e>ƟG\033c:MԧqN\ \010IWbV$nZ(l9c{>̆ٸ8p\025Cvݍ`E\024\012B1;ja6\0379\ ǎM`|\002\012ShZt\002\032Aޗ81?G`fy V94jv\0372\020\017Q\ \001#x\033\033\0057̯z6N\004\021Z;}&bA\036e\036t(\ ;Wht//_?O\011U\011iqǁ^2X\\|wH4F̐K\037a\ 9䌓\013q|`G<\016\032u\032ѯg~\004ύD$i\016\027ӿw\0266\0344\ D\016qitc?\017\007D;rq!NR{\003pŋqX\031\013aIW*\004\007p\ \0315Ju^7?G_?H4v2u}'\0047\037\0011hlő\ XM=\\\000\001ܤ/Wn\014\032p\006&upPt\0106m\031-\003j\035\013\031Az\ \010ηPlŧ\0111]k:i9܎{pI@/{ay:\037\027\024r\005\021GߟONt]k~^'\036e\030&84\003s\ Ͻ![M%m\\ǣʫ\003UD\000sn\014SAљ\036ɖl\035\ \010\016k{@8EA,56`z`3LH]>9M\ ntx%\000q.\020\001\007\00171g9?+\023\001\033\036u*\006Ÿ1q \005\001ԭP_Y\ SW^0-%\024\032cgiaŽxrlIP\011Vݭb\017\024vUHvC\ 'Y\032z{s.\020SḙD\033֗w1S}s7IdxOYe\016ʕ*:һ0g\ %\003\024|\011\007$\027\025\036[\034Zp遫]*\026\012t& \ %\017Bk(\037\005^\005̢\0161_7Aa-\025?{g3x\017J\017m»-zsG\ =h\030.\006\035ocQ\005AU^!x>%z.dB_yOf\030sPX\013\023o\030Gx޸0\ \0269}4/[@zz\015rEth3^ua%)d7\035\021{Y.\022j?\030\027'\ 2}\021v\033:~L\010\0138g:xz&t#\ +ww酱&yR޷KZP\023'\02009˩Q!j-~! \026v2Pl?z:n\ \014m\033=\036\007 n#Eۮ\0159\022dx\0214\0063ތj=1UQ\030֋D\ ol0\031#?g\020\"C4\0368\025\030lE6z\012p2vc5\0171\0263\ uNyf``\000\010_+2PG@jfX0a~\007t\002A}\003\006,\002d\ Rd\026%\034߿\014\017{D=\015\027L\014Bbkv\0367\013O|15?\014]u<=-\031oF\031\ b_93|\031ڟ$\024\014UMcRQ`,tm\017fϣ92iga~\014/\ t\002MǏz\033{4}\025dbU\ g\024\0050a΄\031?\012\002UX:\036eҽ\005wH") ### end spambayes-1.1a6/spambayes/core_resources/ui_psp.py0000664000076500000240000004677610646440122022563 0ustar skipstaff00000000000000# -*- coding: ISO-8859-1 -*- """Resource ui_psp (from file ui.psp)""" # written by resourcepackage: (1, 0, 0) source = 'ui.psp' package = 'spambayes.resources' data = "Paint Shop Pro Image File\012\032\000\000\000\000\000\005\000\000\000~BK\000\000\000.\000\000\000.\000\000\000\001\000\000F\000\000\000j\ tX<@\002\002\000\030\000\001\000\000\000\000\001\000`\002\000\001\000\000\000\002\000\003\000\000%~BK\000\012\000\030\000\000\000~FL\000\001\000\016\000\000\000\000\012\000\000\000\ \012\000\000\000\000\000~BK\000\001\0008\000\000\000~FL\000\001\000\004\000\000\000'>~FL\000\002\000\004\000\000\000\021}?~FL\000\006\000\004\000\000\000\001\000\000\000~F\ L\000\007\000\004\000\000\000\004\004\000\007~BK\000\020\000g\035\000\000\010\000\000\000\002\000\000\000~BK\000\021\000\030\000\000\000\030\000\000\000\000\000\000\037\000\000\000\030\000\003\000\001\000\000\000\ \000\001\001\000~BK\000\021\000\030\000\000\000\030\000\000\000\001\000\000F\000\000\000\030\000\003\000\001\000\000\000\000\001\000\000~BK\000\022\000\012\000\000\016\000\000\000\012\000\000H\000\000\ \005\000\000\020JFIF\000\001\001\000\001,\001,\000\000\000C\000\002\001\001\001\001\001\002\001\001\001\002\002\002\002\002\004\003\002\002\002\002\005\004\004\003\004\006\005\006\006\006\005\006\ \006\006\007\011\010\006\007\011\007\006\006\010\013\010\011\012\012\012\012\012\006\010\013\014\013\012\014\011\012\012\012\000C\001\002\002\002\002\002\002\005\003\003\005\012\007\006\007\012\012\012\012\012\012\012\012\012\012\ \012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\012\000\021\010\000\037\000\003\001\"\000\002\021\001\003\021\001\ \000\033\000\001\000\002\003\001\001\000\000\000\000\000\000\000\000\000\000\000\006\007\003\005\010\004\011\0005\020\000\002\002\001\004\001\002\004\002\007\011\000\000\000\000\000\001\002\003\004\005\000\006\007\021\022\010\ !\023\024\"1\025Q\011\026\027#2A8BUaqv\000\031\001\000\003\001\001\001\000\000\000\000\000\000\000\000\000\000\000\000\003\005\002\006\001\000*\021\000\002\001\003\004\ \000\005\004\003\001\000\000\000\000\000\000\001\002\021\003\004!\000\005\0221\006\"AQa\023Bq#2\000\014\003\001\000\002\021\003\021\000?\000}]ٵbmg\ :2ѣ5Khbݒuw1:$x _8}A~o[Px{,ļ[\ N\0362G\031\";dV2\004c\022t#%z\013VqRq\001pHe\007\004}r\021G7Ms\ hSif\034R\020\004!3\037\036^\017Z淡⨤\011̻t)\0322,kb0z\ \012O\001\004j\027ӷN*4N{\0033F{\001T\005\000\011֥OP\ B@NfX\000 \003\002``jЛm[\024\022I\004Ĝ5ZdOF\ v.~\000* \020Am=E\036\034q\015\004V\034ʮզ1C2C\ \033+O~ǏyOio^t6$CjIYm\"ڬ>CHJ!\010`Y\020O,ox_a\ uvZq\005\016\026V9)ɼ (\004\006'7je[Kr!\0134#\025̙- \001GzTS!R\ +-G<\023ƲC4.\031$B;\014{\020A\004\021MV^upĿ\026+[9c'Gkcfi(% \012S\ \"]<#DO\0217\\\012\026oJNj\014\0060H$AGeZŪT\030}25P?W\ [gwp㰱\013=lf;%\025y&E\030:\016\026wx\027^-z]J\ Y\020RےeHI\0260C1 \020\030Qj6;kf\017\026P2\001F5+\ [ݻ\034\000A\\4GjNTs\017 >\000zJ\020QI^\037/xѺ[=4&\004\017>5DI\"r~O]O%N\\~FSמ6x&\ 2H:*} A\0274p\037\037\010wN5\033C\0126#AD@\033\\\021a}ڜѻ\ TeV\002@?;\000t+@̽\022\001#O_暏ffg,k(Jfj7/+\ \033\011S#O1_.2_s\016]*ը\022iRq\037n\032U\025\024\020=3&⻛k\000ӭ\023\015\ ۘj{\007I+RV:F>E\012?(\003kզR䏒NNޑ@\017\003M4IӴM4\ hQ\037G\021Ǩozc\017Wo>Wþ|~/\037mKӨ\\[NW*`*{\ \006;\007\034\035&\013?U\003q!`=4M'NM4ѣ_~BK\000\022\000\014\022\000\000\016\000\000\000\021\000\000q\001\ \000\010\000\000\020JFIF\000\001\001\000\001,\001,\000\000\000C\000\005\003\004\004\004\003\005\004\004\004\005\005\005\006\007\014\010\007\007\007\007\017\013\013\011\014\021\017\022\022\021\017\ \021\021\023\026\034\027\023\024\032\025\021\021\030!\030\032\035\035\037\037\037\023\027\"$\"\036$\034\036\037\036\000C\001\005\005\005\007\006\007\016\010\010\016\036\024\021\024\036\036\036\036\036\036\036\036\036\ \036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\036\000\021\010\000F\001\003\001\"\000\002\021\001\003\021\001\ \000\034\000\001\000\003\000\003\001\001\000\000\000\000\000\000\000\000\000\000\005\006\007\003\004\010\001\002\000F\020\000\001\003\003\002\003\005\003\010\006\006\013\000\000\000\000\001\000\002\003\004\005\021\006\022\ \007!1\023\024\"AQ\0252a\010\026#Bq3Vb\027$&CR78Ffuv\000\033\001\001\000\003\001\001\001\001\000\000\000\000\000\000\ \000\000\000\000\004\005\006\003\002\001\007\0002\021\000\001\003\002\004\004\004\006\002\002\003\000\000\000\000\000\001\000\002\021\003\004\005!1Q\022\023Aa\"q2R\024B\006\025\ \000\014\003\001\000\002\021\003\021\000?\000Z\"\"\"\"\"\"(\015w-Z:L\003Og\000U\006{ӷK\ \035uFv(Q\020\031?i$g]D0\020'sDu\026\013i1舡)h\ \023\034\020ICW\017PǨSȠ~z\017\030rSj)S\ Q\03556M4\0148sp\000\001$XCi\\fX}\012\013' \024\"(\"*\005\004j\033K'gUQO\ t'R\001N:\022\"8:\016cO2\007U\036~Q\0222>ߺ\002z+/s\\摐A\ !}QԄDDDEXk[mRGqۊw(R\011\\4\027`\022ZH0\0276\026ƪNƶ\007\ >\011'-$\034\0232\024e\\S\026\013%Յ\021\024U%\021\026W2[k5W\010|39\ \024N\000\016G7\021\0061뜅.*d,+:\002\021a\016':l^u+MAnǴ=\ \031\031?д>\032q\002Ѯhe}\033$xZ\017G4y\036_\0209)wX-յ.q\000dKH \036[b\ \025y `\011oDER\021\021\021\021\021\021\021\021\021\021\021\026GUu\027)t˂;f3>-p~\ c|\036/\035özoZBEmN=浻\001h\033\032R\ !p6:I\0076\031(\0378;;A\024\033v\026\017Rњ\021\026Mi\ \021\021\024\006v!buʵGߞCѣ>_U\003f]q\037Pۢyl6)\020D6:B^n\ !q$\006\0175cff*\015\035ֆNT?c<\034\0160\017\025\003R6îZoTqG)\ kc|R\006\001\014.G\010$\022|\014\037E|t\021\025\006#T\000\004\016\037]\007#ܪ>\ ZgLXu&U\006I_%`q8\016kZ׆0\\\036N\006pz-\002\035)cRXoF\007wMNҢ\ Ų\006Ip\037,nݧl\002T6)\011dry\016ŀe\000[-\036-\016\031\ +*t^\033|tˆ\\w=`j1Ju\005:L>0>\"Cc|\007\0137Q\ i\004dGOZv[%knFy9k\007PZ\017K\0155Oe%|`!\ \000\000\000\005:<\024g\012&[&'X跶橤Tx8\024fncf3xJEΛ\034\ \0345\013\036a\034A-ꚶQTұԳ1K\021z;\024\000@9M\0335uoa\ \016\003\031\001>\033\006\002y\023(\00388E{c4^m-ɒ*j!%Xv\034;o23[6d\ ;[()\033<\000H\"4%9\007V\035@b\024\011\035\004fc/E6\025\"@D\ M\004\010kPśmGH-r:\0127g\033d?h\0334\033=p\006\ |\015qi\0008S5t9}NJ\026\"2֣c-v\002=A=\031\023s$xq$\ CX=\017\025)nԚJJxëds\011qcy\035,^FK\023%{\036湧 Ѕ\ \007\025\037kgWx%Kx\0073\030BXY#\001\021ovnRn\000[Y\ L`\003q\005wz:z 5T\001\005c]{\015\025ʑw\032:zg\031\02219\031i\004\036|\032\0259u\003|q]0꫺\ M-V\012vJO\015\024l,Mc,f2\017?\017,s\035U\"\013N{d,̨\035\003\014\ Q\035!#;N0Ei\032FشCa\022\036[;qɮĎ\013m˝=pm5l\ u\0011\007dNn4\035 s+kU#fu'e}\016c^SqCPpx]\0152g]F\ GN\025nDEBf\027\010iKSpzʉ*$lsD\032\034\027\0203\0318+OQ:\015\037z6o\ &<ٻhh\0340\002awqoR(?\004w.)fq\006汮=2q\013(5n|mMH\037Fݎ\016\ 恞霮ɛFݭsk\022RGSM顐a4/#x@\036>X#2%Y{T1\ I\036\031\031\033\\XIsq\006>x^Zj\027j0LÄ=\0367[Į]dZaDD\ XDDDDDDDDDDDDE\037KOi]yីH\"׹bW1!0\ HiઅT\024;d\016iA\\A\020\016EB7Hi6KڷK\033&sPD\017L\033\"\ Ʊ\030kZ0\000\005EjTO^YI@\010Y{$qI\015#c\0322\\x\003\ 媷jn\030E\024{E=\\ѳpA!\036.#\005i'Z\026\\\035;e\032(.\007h9DDP\ \024\\\027\032:k\0045jj\034F\010\\辂Zd/\002 Sp=[n:Fv4\ F}f'\016\003/U[w\030S\003\005\024\024&\022\003[\0333W\021_[m*q|3Ƿ3FZNFA\007S\012\024\\k\037YpUu2c|J\000\001\036g\000\001++*\ \\>Z\022rv~U_F2-!\014۰R(V2W*cɠnA\007\016\034A#PN\ 6\013F_-7TнUW\006\032IH\031\"Re:k\033\000)Ri\002\000;7\021\025\ jDDDDDDDDD_cd:)XZd8\036]v\0329KL3IݠD\016\0040o\\OW?p\ \".1Ɔ':\\?(ꋥbWӚ8GY\034֒\033E\007\002D\ .\004\001p_ڻQZ.EWGT\036C k;֗\002\013FH\015g淕ҠZm\023TP[(&i \ t\031?z\014V2!M\004\015PpZx+2s?dDEZ\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\ \021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\ \021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\ \021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\027~BK\000\003\0008\007\000\000~\ BK\000\004\000\001\000\000\000\000\000\012\000Background\001\000\000\000\000\000\000\000\000\001\000\000F\000\000\000\000\000\000\000\000\000\000\000\001\000\000F\000\000\000\000\ \001\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\001\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\010\000\000\000\001\000\003\000~BK\000\005\000L\000\000\000\020\000\000\000<\000\000\000q\001\000\000\000\001\000x\000\ \000\000\000 ]\010U\001\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\0007\000\000\000\003\000%\033~BK\000\005\000\ L\000\000\000\020\000\000\000<\000\000\000q\001\000\000\000\002\000x\000\000\000\000 ]\010U\001\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\0007\000\000\000\003\000%\033~BK\000\005\000L\000\000\000\020\000\000\000<\000\000\000q\001\000\000\000\003\000x\000\000\000\000 ]\ \010U\001\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\0007\000\000\000\003\000%\033~BK\000\004\000\005\000\000}\000\000\000\ \004\000Text\003\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\001\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\001\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ ~BK\000\015\000\010\005\000\000\010\000\000\000\001\000\000\000~BK\000\016\000\004\000\000\024\000\000\000\000\000\001\000\007\000\000\000\001\000\000\000\000\000\000\000U\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000?\000\000\000\000\000\000\000\000\000\000\000\000\000\000\034@\000\000\000\000\000\000\000\000\000\000\000\000\000\000?\000\000\000\000\000\000B@\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000?\010\000\000\000\016\000\000\000\006\000\000\000\003\000\010\000\000\000\000\000\000\000\006\000\000\000\002\000Y\000\000\000\010\000Webdings\000\000\000\000\ \001\000\000\002\000\000\0005\000\000\000\000UUUUUU5@\000\001\000\001\000\000\001ףp=\012\034@ףp=\012\034@\000\000\000\000\000\000\000\000?\000\000\000\000\000\000\ ?\000\000\000\000\000\000\000$@~BK\000\017\000\022\000\000\000\006\000\000\000\001\000\014\000\000\000\000\000\000\000~BK\000\017\000\022\000\000\000\006\000\000\000\001\000\014\000\000\000\000\ \000~BK\000\023\000-\000\000\000-\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\006\000\000\000\001\000\010\000\000\000a\000\000\000\006\000\000\000\002\000Y\000\000\000\010\000Webdings\000\000\000\000\001\000\000\002\000\000\000%\000\000\000\000-\ @\000\001\000\001\000\000\001ףp=\012\034@ףp=\012\034@\000\000\000\000\000\000\000\000?\000\000\000\000\000\000?\000\000\000\000\000\000\000$@~BK\000\017\000\022\000\000\ \000\006\000\000\000\001\000\014\000\000\000\000\000\000\000~BK\000\017\000\022\000\000\000\006\000\000\000\001\000\014\000\000\000\000\000~BK\000\023\000-\000\000\000-\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\006\000\000\000\001\000\010\000\000\000N\000\000\000\006\000\000\000\ \001\000\010\000\000\000L\000\000\000\006\000\000\000\001\000\010\000\000\000i\000\000\000\006\000\000\000\002\000Y\000\000\000\010\000Webdings\000\000\000\000\002\000\000\002\000\000\000%\000\000\000\ \000-@\000\001\000\001\000\000\001ףp=\012\034@ףp=\012\034@\000\000\000\000\000\000\000\000?\000\000\000\000\000\000?\000\000\000\000\000\000\000$@~\ BK\000\017\000\022\000\000\000\006\000\000\000\001\000\014\000\000\000\000\000\000\000~BK\000\017\000\022\000\000\000\006\000\000\000\001\000\014\000\000\000\000\000~BK\000\023\ \000-\000\000\000-\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\006\000\000\000\001\000\010\000\000\000\ \000\000\000\006\000\000\000\002\000Z\000\000\000\011\000Wingdings\000\000\000\000\001\000\000\002\000\000\000%\000\000\000\000-@\000\001\000\001\000\000\001ףp\ =\012\034@ףp=\012\034@\000\000\000\000\000\000\000\000?\000\000\000\000\000\000?\000\000\000\000\000\000\000$@~BK\000\017\000\022\000\000\000\006\000\000\000\001\000\014\000\000\000\ \000\000\000\000~BK\000\017\000\022\000\000\000\006\000\000\000\001\000\014\000\000\000\000\000~BK\000\023\000-\000\000\000-\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\006\000\000\000\001\000\010\000\000\000C\000\000\000\006\000\000\000\001\000\010\000\000\000D\000\000\000\006\ \000\000\000\002\000Y\000\000\000\010\000Webdings\000\000\000\000\001\000\000\002\000\000\000#\000\000\000\000+@\000\001\000\001\000\000\001ףp=\012\034@\ p=\012\034@\000\000\000\000\000\000\000\000?\000\000\000\000\000\000?\000\000\000\000\000\000\000$@~BK\000\017\000\022\000\000\000\006\000\000\000\001\000\014\000\000\000\000\000\000\000\ ~BK\000\017\000\022\000\000\000\006\000\000\000\001\000\014\000\000\000\000\000~BK\000\023\000-\000\000\000-\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\006\000\000\000\001\000\010\000\000\000s\000\000\000\010\000\000\000\000\000\000\000~BK\000\006\000r\000\000\000$\000\000\ \000o\001\000\000\005\000\000\000\001\000\000.\000\000\000\000\000\000\000\000\000\000\000)\000\000\000)\000\000\000\010\000\000\000\001\000\001\000~BK\000\005\000<\000\000\000\020\000\000\000,\000\000\0004\ \013\000\000\003\000\000\000xJG\034H80oTQ\012G\025*D(I$\016\000\000\000\000\003\0003C" ### end spambayes-1.1a6/spambayes/CorePlugin.py0000664000076500000240000000040710646440130020265 0ustar skipstaff00000000000000""" Plugins for Core Server. """ __author__ = "Skip Montanaro OK. Return Home.

    ")) def _buildReviewKeys(self, timestamp): """Builds an ordered list of untrained message keys, ready for output in the Review list. Returns a 5-tuple: the keys, the formatted date for the list (eg. "Friday, November 15, 2002"), the start of the prior page or zero if there isn't one, likewise the start of the given page, and likewise the start of the next page.""" # Fetch all the message keys allKeys = self.state.unknownCorpus.keys() # We have to sort here to split into days. # Later on, we also sort the messages that will be on the page # (by whatever column we wish). allKeys.sort() # The default start timestamp is derived from the most recent message, # or the system time if there are no messages (not that it gets used). if not timestamp: if allKeys: timestamp = self._keyToTimestamp(allKeys[-1]) else: timestamp = time.time() start, end, date = self._getTimeRange(timestamp) # Find the subset of the keys within this range. startKeyIndex = bisect.bisect(allKeys, "%d" % long(start)) endKeyIndex = bisect.bisect(allKeys, "%d" % long(end)) keys = allKeys[startKeyIndex:endKeyIndex] keys.reverse() # What timestamps to use for the prior and next days? If there any # messages before/after this day's range, use the timestamps of those # messages - this will skip empty days. prior = end = 0 if startKeyIndex != 0: prior = self._keyToTimestamp(allKeys[startKeyIndex-1]) if endKeyIndex != len(allKeys): end = self._keyToTimestamp(allKeys[endKeyIndex]) # Return the keys and their date. return keys, date, prior, start, end def onReview(self, **params): """Present a list of message for (re)training.""" # Train/discard sumbitted messages. self._writePreamble("Review") id = '' numTrained = 0 numDeferred = 0 if params.get('go') != _('Refresh'): for key, value in params.items(): if key.startswith('classify:'): old_class, id = key.split(':')[1:3] if value == _('spam'): targetCorpus = self.state.spamCorpus stats_as_ham = False elif value == _('ham'): targetCorpus = self.state.hamCorpus stats_as_ham = True elif value == _('discard'): targetCorpus = None try: self.state.unknownCorpus.removeMessage( self.state.unknownCorpus[id]) except KeyError: pass # Must be a reload. else: # defer targetCorpus = None numDeferred += 1 if targetCorpus: sourceCorpus = None if self.state.unknownCorpus.get(id) is not None: sourceCorpus = self.state.unknownCorpus elif self.state.hamCorpus.get(id) is not None: sourceCorpus = self.state.hamCorpus elif self.state.spamCorpus.get(id) is not None: sourceCorpus = self.state.spamCorpus if sourceCorpus is not None: try: # fromCache is a fix for sf #851785. # See the comments in Corpus.py targetCorpus.takeMessage(id, sourceCorpus, fromCache=True) if numTrained == 0: self.write(_("

    Training... ")) self.flush() numTrained += 1 self.stats.RecordTraining(\ stats_as_ham, old_class=old_class) except KeyError: pass # Must be a reload. # Report on any training, and save the database if there was any. if numTrained > 0: plural = '' if numTrained == 1: response = "Trained on one message. " else: response = "Trained on %d messages. " % (numTrained,) self._doSave() self.write(response) self.write("
     ") title = "" keys = [] sourceCorpus = self.state.unknownCorpus # If any messages were deferred, show the same page again. if numDeferred > 0: start = self._keyToTimestamp(id) # Else after submitting a whole page, display the prior page or the # next one. Derive the day of the submitted page from the ID of the # last processed message. elif id: start = self._keyToTimestamp(id) unused, unused, prior, unused, next = self._buildReviewKeys(start) if prior: start = prior else: start = next # Else if they've hit Previous or Next, display that page. elif params.get('go') == _('Next day'): start = self._keyToTimestamp(params['next']) elif params.get('go') == _('Previous day'): start = self._keyToTimestamp(params['prior']) # Else if an id has been specified, just show that message # Else if search criteria have been specified, show the messages # that match those criteria. elif params.get('find') is not None: prior = next = 0 keys = set() # so we don't end up with duplicates push = keys.add try: max_results = int(params['max_results']) except ValueError: max_results = 1 key = params['find'] if params.has_key('ignore_case'): ic = True else: ic = False error = False if key == "": error = True page = _("

    You must enter a search string.

    ") else: if len(keys) < max_results and \ params.has_key('id'): if self.state.unknownCorpus.get(key): push((key, self.state.unknownCorpus)) elif self.state.hamCorpus.get(key): push((key, self.state.hamCorpus)) elif self.state.spamCorpus.get(key): push((key, self.state.spamCorpus)) if params.has_key('subject') or params.has_key('body') or \ params.has_key('headers'): # This is an expensive operation, so let the user know # that something is happening. self.write(_('

    Searching...

    ')) for corp in [self.state.unknownCorpus, self.state.hamCorpus, self.state.spamCorpus]: for k in corp.keys(): if len(keys) >= max_results: break msg = corp[k] msg.load() if params.has_key('subject'): subj = str(msg['Subject']) if self._contains(subj, key, ic): push((k, corp)) if params.has_key('body'): # For [ 906581 ] Assertion failed in search # subject. Can the headers be a non-string? msg_body = msg.as_string() msg_body = msg_body[msg_body.index('\r\n\r\n'):] if self._contains(msg_body, key, ic): push((k, corp)) if params.has_key('headers'): for nm, val in msg.items(): # For [ 906581 ] Assertion failed in # search subject. Can the headers be # a non-string? nm = str(nm) val = str(val) if self._contains(nm, key, ic) or \ self._contains(val, key, ic): push((k, corp)) if len(keys): if len(keys) == 1: title = _("Found message") else: title = _("Found messages") keys = list(keys) else: page = _("

    Could not find any matching messages. " \ "Maybe they expired?

    ") title = _("Did not find message") box = self._buildBox(title, 'status.gif', page) self.write(box) self.write(self._buildBox(_('Find message'), 'query.gif', self.html.findMessage)) self._writePostamble() return # Else show the most recent day's page, as decided by _buildReviewKeys. else: start = 0 # Build the lists of messages: spams, hams and unsure. if len(keys) == 0: keys, date, prior, this, next = self._buildReviewKeys(start) keyedMessageInfo = {options["Headers", "header_unsure_string"]: [], options["Headers", "header_ham_string"]: [], options["Headers", "header_spam_string"]: [], } invalid_keys = [] for key in keys: if isinstance(key, types.TupleType): key, sourceCorpus = key else: sourceCorpus = self.state.unknownCorpus # Parse the message, get the judgement header and build a message # info object for each message. message = sourceCorpus[key] try: message.load() except IOError: # Someone has taken this file away from us. It was # probably a virus protection program, so that's ok. # Don't list it in the review, though. invalid_keys.append(key) continue judgement = message[options["Headers", "classification_header_name"]] if judgement is None: judgement = options["Headers", "header_unsure_string"] else: judgement = judgement.split(';')[0].strip() messageInfo = self._makeMessageInfo(message) keyedMessageInfo[judgement].append((key, messageInfo)) for key in invalid_keys: keys.remove(key) # Present the list of messages in their groups in reverse order of # appearance, by default, or according to the specified sort order. if keys: page = self.html.reviewtable.clone() if prior: page.prior.value = prior del page.priorButton.disabled if next: page.next.value = next del page.nextButton.disabled templateRow = page.reviewRow.clone() # The decision about whether to reverse the sort # order has to go here, because _sortMessages gets called # thrice, and so the ham list would end up sorted backwards. sort_order = params.get('sort') if self.previous_sort == sort_order: reverse = True self.previous_sort = None else: reverse = False self.previous_sort = sort_order page.table = "" # To make way for the real rows. for header, label in ((options["Headers", "header_unsure_string"], 'Unsure'), (options["Headers", "header_ham_string"], 'Ham'), (options["Headers", "header_spam_string"], 'Spam')): messages = keyedMessageInfo[header] if messages: sh = self.html.reviewSubHeader.clone() # Setup the header row sh.optionalHeaders = '' h = self.html.headerHeader.clone() for disp_header in options["html_ui", "display_headers"]: h.headerLink.href = 'review?sort=%sHeader' % \ (disp_header.lower(),) h.headerName = disp_header.title() sh.optionalHeaders += h if not options["html_ui", "display_score"]: del sh.score_header if not options["html_ui", "display_received_time"]: del sh.received_header subHeader = str(sh) subHeader = subHeader.replace('TYPE', label) page.table += self.html.blankRow page.table += subHeader self._appendMessages(page.table, messages, label, sort_order, reverse) page.table += self.html.trainRow if title == "": title = _("Untrained messages received on %s") % date box = self._buildBox(title, None, page) # No icon, to save space. else: page = _("

    There are no untrained messages to display. " \ "Return Home, or " \ "check again.

    ") title = _("No untrained messages") box = self._buildBox(title, 'status.gif', page) self.write(box) self._writePostamble(help_topic="review") def onView(self, key, corpus): """View a message - linked from the Review page.""" self._writePreamble(_("View message"), parent=('review', _('Review'))) sourceCorpus = None message = None if self.state.unknownCorpus.get(key) is not None: sourceCorpus = self.state.unknownCorpus elif self.state.hamCorpus.get(key) is not None: sourceCorpus = self.state.hamCorpus elif self.state.spamCorpus.get(key) is not None: sourceCorpus = self.state.spamCorpus if sourceCorpus is not None: message = sourceCorpus.get(key) if message is not None: self.write("
    %s
    " % cgi.escape(message.as_string())) else: self.write(_("

    Can't find message %r. Maybe it expired.

    ") % key) self._writePostamble() def onShowclues(self, key, subject, tokens='0'): """Show clues for a message - linked from the Review page.""" tokens = bool(int(tokens)) # needs the int, as bool('0') is True self._writePreamble(_("Message clues"), parent=('review', _('Review'))) sourceCorpus = None message = None if self.state.unknownCorpus.get(key) is not None: sourceCorpus = self.state.unknownCorpus elif self.state.hamCorpus.get(key) is not None: sourceCorpus = self.state.hamCorpus elif self.state.spamCorpus.get(key) is not None: sourceCorpus = self.state.spamCorpus if sourceCorpus is not None: message = sourceCorpus.get(key).as_string() if message is not None: # For Macs? message = message.replace('\r\n', '\n').replace('\r', '\n') results = self._buildCluesTable(message, subject, tokens) del results.classifyAnother self.write(results) else: self.write(_("

    Can't find message %r. Maybe it expired.

    ") % key) self._writePostamble() def onPluginconfig(self): html = self._buildConfigPage(self.plugin.plugin_map) html.title = _('Home > Plugin Configuration') html.pagename = _('> Plugin Configuration') html.plugin_button.name.value = _("Back to basic configuration") html.plugin_button.action = "config" html.config_submit.value = _("Save plugin options") html.restore.value = _("Restore plugin options defaults") del html.exp_button del html.adv_button self.writeOKHeaders('text/html') self.write(html) def close_database(self): self.state.close() def reReadOptions(self): """Called by the config page when the user saves some new options, or restores the defaults.""" load_options() # Recreate the state. self.state = self.state.recreate_state() self.classifier = self.state.bayes def verifyInput(self, parms, pmap): '''Check that the given input is valid.''' # Most of the work here is done by the parent class, but # we have a few extra checks errmsg = UserInterface.UserInterface.verifyInput(self, parms, pmap) if pmap != parm_ini_map: return errmsg return errmsg def readUIResources(self): """Returns ui.html and a dictionary of Gifs.""" if self.lang_manager: ui_html = self.lang_manager.import_ui_html() else: from spambayes.core_resources import ui_html images = {} for baseName in UserInterface.IMAGES: moduleName = '%s.%s_gif' % ('spambayes.core_resources', baseName) module = __import__(moduleName, {}, {}, ('spambayes', 'core_resources')) images[baseName] = module.data return ui_html.data, images class CoreState: """This keeps the global state of the module - the command-line options, statistics like how many mails have been classified, the handle of the log file, the Classifier and FileCorpus objects, and so on.""" def __init__(self): """Initialises the State object that holds the state of the app. The default settings are read from Options.py and bayescustomize.ini and are then overridden by the command-line processing code in the __main__ code below.""" self.log_file = None self.bayes = None self.mutex = None self.prepared = False self.can_stop = True self.plugin = None # Unique names for cached messages - see `getNewMessageName()` below. self.last_base_message_name = '' self.uniquifier = 2 # Set up the statistics. self.numSpams = 0 self.numHams = 0 self.numUnsure = 0 self.servers = "" # Load up the other settings from Option.py / bayescustomize.ini self.ui_port = options["html_ui", "port"] self.launch_ui = options["html_ui", "launch_browser"] self.gzip_cache = options["Storage", "cache_use_gzip"] self.run_test_server = False self.is_test = False self.spamCorpus = self.hamCorpus = self.unknownCorpus = None self.spam_trainer = self.ham_trainer = None self.init() def init(self): assert not self.prepared, "init after prepare, but before close" ## no i18n yet... ## # Load the environment for translation. ## self.lang_manager = i18n.LanguageManager() ## # Set the system user default language. ## self.lang_manager.set_language(\ ## self.lang_manager.locale_default_lang()) ## # Set interface to use the user language in the configuration file. ## for language in reversed(options["globals", "language"]): ## # We leave the default in there as the last option, to fall ## # back on if necessary. ## self.lang_manager.add_language(language) ## if options["globals", "verbose"]: ## print "Asked to add languages: " + \ ## ", ".join(options["globals", "language"]) ## print "Set language to " + \ ## str(self.lang_manager.current_langs_codes) self.lang_manager = None # Open the log file. if options["globals", "verbose"]: self.log_file = open('_core_server.log', 'wb', 0) # Remember reported errors. self.reported_errors = {} def close(self): assert self.prepared, "closed without being prepared!" if self.bayes is not None: # Only store a non-empty db. if self.bayes.nham != 0 and self.bayes.nspam != 0: self.bayes.store() self.bayes.close() self.bayes = None spambayes.message.Message().message_info_db = None self.spamCorpus = self.hamCorpus = self.unknownCorpus = None self.spam_trainer = self.ham_trainer = None self.prepared = False self.close_platform_mutex() def prepare(self, can_stop=True): """Do whatever needs to be done to prepare for running. If can_stop is False, then we may not let the user shut down the proxy - for example, running as a Windows service this should be the case.""" self.init() # If we can, prevent multiple servers from running at the same time. assert self.mutex is None, "Should not already have the mutex" self.open_platform_mutex() self.can_stop = can_stop # Do whatever we've been asked to do... self.create_workers() self.prepared = True def build_status_strings(self): """Build the status message(s) to display on the home page of the web interface.""" nspam = self.bayes.nspam nham = self.bayes.nham if nspam > 10 and nham > 10: db_ratio = nham/float(nspam) if db_ratio > 5.0: self.warning = _("Warning: you have much more ham than " \ "spam - SpamBayes works best with " \ "approximately even numbers of ham and " \ "spam.") elif db_ratio < (1/5.0): self.warning = _("Warning: you have much more spam than " \ "ham - SpamBayes works best with " \ "approximately even numbers of ham and " \ "spam.") else: self.warning = "" elif nspam > 0 or nham > 0: self.warning = _("Database only has %d good and %d spam - " \ "you should consider performing additional " \ "training.") % (nham, nspam) else: self.warning = _("Database has no training information. " \ "SpamBayes will classify all messages as " \ "'unsure', ready for you to train.") # Add an additional warning message if the user's thresholds are # truly odd. spam_cut = options["Categorization", "spam_cutoff"] ham_cut = options["Categorization", "ham_cutoff"] if spam_cut < 0.5: self.warning += _("
    Warning: we do not recommend " \ "setting the spam threshold less than 0.5.") if ham_cut > 0.5: self.warning += _("
    Warning: we do not recommend " \ "setting the ham threshold greater than 0.5.") if ham_cut > spam_cut: self.warning += _("
    Warning: your ham threshold is " \ "higher than your spam threshold. " \ "Results are unpredictable.") def create_workers(self): """Using the options that were initialised in __init__ and then possibly overridden by the driver code, create the Bayes object, the Corpuses, the Trainers and so on.""" if self.is_test: self.use_db = "pickle" self.db_name = '_core_server.pickle' # This is never saved. if not hasattr(self, "db_name"): self.db_name, self.use_db = storage.database_type([]) self.bayes = storage.open_storage(self.db_name, self.use_db) # Load stats manager. self.stats = Stats.Stats(options, spambayes.message.Message().message_info_db) self.build_status_strings() # Don't set up the caches and training objects when running the # self-test, so as not to clutter the filesystem. if not self.is_test: # Create/open the Corpuses. Use small cache sizes to avoid # hogging lots of memory. sc = get_pathname_option("Storage", "core_spam_cache") hc = get_pathname_option("Storage", "core_ham_cache") uc = get_pathname_option("Storage", "core_unknown_cache") for d in [sc, hc, uc]: storage.ensureDir(d) if self.gzip_cache: factory = GzipFileMessageFactory() else: factory = FileMessageFactory() age = options["Storage", "cache_expiry_days"]*24*60*60 self.spamCorpus = ExpiryFileCorpus(age, factory, sc, '[0123456789\-]*', cacheSize=20) self.hamCorpus = ExpiryFileCorpus(age, factory, hc, '[0123456789\-]*', cacheSize=20) self.unknownCorpus = ExpiryFileCorpus(age, factory, uc, '[0123456789\-]*', cacheSize=20) # Given that (hopefully) users will get to the stage # where they do not need to do any more regular training to # be satisfied with spambayes' performance, we expire old # messages from not only the trained corpora, but the unknown # as well. self.spamCorpus.removeExpiredMessages() self.hamCorpus.removeExpiredMessages() self.unknownCorpus.removeExpiredMessages() # Create the Trainers. self.spam_trainer = storage.SpamTrainer(self.bayes) self.ham_trainer = storage.HamTrainer(self.bayes) self.spamCorpus.addObserver(self.spam_trainer) self.hamCorpus.addObserver(self.ham_trainer) def getNewMessageName(self): """The message name is the time it arrived with a uniquifier appended if two arrive within one clock tick of each other. """ message_name = "%10.10d" % long(time.time()) if message_name == self.last_base_message_name: message_name = "%s-%d" % (message_name, self.uniquifier) self.uniquifier += 1 else: self.last_base_message_name = message_name self.uniquifier = 2 return message_name def record_classification(self, cls, score): """Record the classification in the session statistics. cls should match one of the options["Headers", "header_*_string"] values. score is the score the message received. """ if cls == options["Headers", "header_ham_string"]: self.numHams += 1 elif cls == options["Headers", "header_spam_string"]: self.numSpams += 1 else: self.numUnsure += 1 self.stats.RecordClassification(score) def buildStatusStrings(self): return "" def recreate_state(self): if self.prepared: # Close the state (which saves if necessary) self.close() # And get a new one going. state = CoreState() state.prepare() return state def open_platform_mutex(self, mutex_name="SpamBayesServer"): """Implementations of a mutex or other resource which can prevent multiple servers starting at once. Platform specific as no reasonable cross-platform solution exists (however, an old trick is to use a directory for a mutex, as a create/test atomic API generally exists). Will set self.mutex or may throw AlreadyRunningException """ if sys.platform.startswith("win"): try: import win32event, win32api, winerror # ideally, the mutex name could include either the username, # or the munged path to the INI file - this would mean we # would allow multiple starts so long as they weren't for # the same user. However, as of now, the service version # is likely to start as a different user, so a single mutex # is best for now. # XXX - even if we do get clever with another mutex name, we # should consider still creating a non-exclusive # "SpamBayesServer" mutex, if for no better reason than so # an installer can check if we are running try: hmutex = win32event.CreateMutex(None, True, mutex_name) except win32event.error, details: # If another user has the mutex open, we get an "access # denied" error - this is still telling us what we need # to know. if details[0] != winerror.ERROR_ACCESS_DENIED: raise raise AlreadyRunningException # mutex opened - now check if we actually created it. if win32api.GetLastError()==winerror.ERROR_ALREADY_EXISTS: win32api.CloseHandle(hmutex) raise AlreadyRunningException self.mutex = hmutex return except ImportError: # no win32all - no worries, just start pass self.mutex = None def close_platform_mutex(self): """Toss out the current mutex.""" if sys.platform.startswith("win"): if self.mutex is not None: self.mutex.Close() self.mutex = None spambayes-1.1a6/spambayes/Corpus.py0000664000076500000240000002300111116605713017467 0ustar skipstaff00000000000000#! /usr/bin/env python '''Corpus.py - Spambayes corpus management framework. Classes: Corpus - a collection of Messages ExpiryCorpus - a "young" Corpus MessageFactory - creates a Message Abstract: A corpus is defined as a set of messages that share some common characteristic relative to spamness. Examples might be spam, ham, unsure, or untrained, or "bayes rating between .4 and .6". A corpus is a collection of messages. Corpus is a dictionary that is keyed by the keys of the messages within it. It is iterable, and observable. Observers are notified when a message is added to or removed from the corpus. Corpus is designed to cache message objects. By default, it will only engage in lazy creation of message objects, keeping those objects in memory until the corpus instance itself is destroyed. In large corpora, this could consume a large amount of memory. A cacheSize operand is implemented on the constructor, which is used to limit the *number* of messages currently loaded into memory. The instance variable that implements this cache is Corpus.Corpus.msgs, a dictionary. Access to this variable should be through keys(), [key], or using an iterator. Direct access should not be used, as subclasses that manage their cache may use this variable very differently. Iterating Corpus objects is potentially very expensive, as each message in the corpus will be brought into memory. For large corpora, this could consume a lot of system resources. ExpiryCorpus is designed to keep a corpus of file messages that are guaranteed to be younger than a given age. The age is specified on the constructor, as a number of seconds in the past. If a message file was created before that point in time, the a message is deemed to be "old" and thus ignored. Access to a message that is deemed to be old will raise KeyError, which should be handled by the corpus user as appropriate. While iterating, KeyError is handled by the iterator, and messages that raise KeyError are ignored. As messages pass their "expiration date," they are eligible for removal from the corpus. To remove them properly, removeExpiredMessages() should be called. As messages are removed, observers are notified. ExpiryCorpus function is included into a concrete Corpus through multiple inheritance. It must be inherited before any inheritance that derives from Corpus. For example: class RealCorpus(Corpus) ... class ExpiryRealCorpus(Corpus.ExpiryCorpus, RealCorpus) ... Messages have substance, which is is the textual content of the message. They also have a key, which uniquely defines them within the corpus. This framework makes no assumptions about how or if messages persist. MessageFactory is a required factory class, because Corpus is designed to do lazy initialization of messages and, as an abstract class, must know how to create concrete instances of the correct class. To Do: o Suggestions? ''' # This module is part of the spambayes project, which is Copyright 2002-2007 # The Python Software Foundation and is covered by the Python Software # Foundation license. from __future__ import generators __author__ = "Tim Stone " __credits__ = "Richie Hindle, Tim Peters, all the spambayes contributors." import sys # for output of docstring import time from spambayes.Options import options SPAM = True HAM = False class Corpus: '''An observable dictionary of Messages''' def __init__(self, factory, cacheSize=-1): '''Constructor(MessageFactory)''' self.msgs = {} # dict of all messages in corpus # value is None if msg not currently loaded self.keysInMemory = [] # keys of messages currently loaded # this *could* be derived by iterating msgs self.cacheSize = cacheSize # max number of messages in memory self.observers = [] # observers of this corpus self.factory = factory # factory for the correct Message subclass def addObserver(self, observer): '''Register an observer, which should implement onAddMessage, onRemoveMessage''' self.observers.append(observer) def addMessage(self, message, observer_flags=0): '''Add a Message to this corpus''' if options["globals", "verbose"]: print 'adding message %s to corpus' % (message.key()) self.cacheMessage(message) for obs in self.observers: # there is no reason that a Corpus observer MUST be a Trainer # and so it may very well not be interested in AddMessage events # even though right now the only observable events are # training related if hasattr(obs, "onAddMessage"): obs.onAddMessage(message, observer_flags) def removeMessage(self, message, observer_flags=0): '''Remove a Message from this corpus''' key = message.key() if options["globals", "verbose"]: print 'removing message %s from corpus' % (key,) self.unCacheMessage(key) del self.msgs[key] for obs in self.observers: # see comments in event loop in addMessage if hasattr(obs, "onRemoveMessage"): obs.onRemoveMessage(message, observer_flags) def cacheMessage(self, message): '''Add a message to the in-memory cache''' # This method should probably not be overridden key = message.key() if options["globals", "verbose"]: print 'placing %s in corpus cache' % (key,) self.msgs[key] = message # Here is where we manage the in-memory cache size... self.keysInMemory.append(key) if self.cacheSize > 0: # performance optimization if len(self.keysInMemory) > self.cacheSize: keyToFlush = self.keysInMemory[0] self.unCacheMessage(keyToFlush) def unCacheMessage(self, key): '''Remove a message from the in-memory cache''' # This method should probably not be overridden if options["globals", "verbose"]: print 'Flushing %s from corpus cache' % (key,) try: ki = self.keysInMemory.index(key) except ValueError: pass else: del self.keysInMemory[ki] self.msgs[key] = None def takeMessage(self, key, fromcorpus, fromCache=False): '''Move a Message from another corpus to this corpus''' msg = fromcorpus[key] msg.load() # ensure that the substance has been loaded # Remove needs to be first, because add changes the directory # of the message, and so remove won't work then. fromcorpus.removeMessage(msg) self.addMessage(msg) def get(self, key, default=None): if self.msgs.get(key, "") == "": return default else: return self[key] def __getitem__(self, key): '''Corpus is a dictionary''' amsg = self.msgs.get(key, "") if amsg == "": raise KeyError(key) if amsg is None: amsg = self.makeMessage(key) # lazy init, saves memory self.cacheMessage(amsg) return amsg def keys(self): '''Message keys in the Corpus''' return self.msgs.keys() def __contains__(self, other): return other in self.msgs.values() def __iter__(self): '''Corpus is iterable''' for key in self.keys(): yield self[key] def __str__(self): '''Instance as a printable string''' return self.__repr__() def __repr__(self): '''Instance as a representative string''' raise NotImplementedError def makeMessage(self, key, content=None): '''Call the factory to make a message''' # This method will likely be overridden msg = self.factory.create(key, content) return msg class ExpiryCorpus: '''Mixin Class - Corpus of "young" file system artifacts''' def __init__(self, expireBefore): self.expireBefore = expireBefore # Only check for expiry after this time. self.expiry_due = time.time() def removeExpiredMessages(self): '''Kill expired messages''' # Only check for expired messages after this time. We set this to the # closest-to-expiry message's expiry time, so that this method can be # called very regularly, and most of the time it will just immediately # return. if time.time() < self.expiry_due: return self.expiry_due = time.time() + self.expireBefore for key in self.keys()[:]: msg = self[key] timestamp = msg.createTimestamp() if timestamp < time.time() - self.expireBefore: if options["globals", "verbose"]: print 'message %s has expired' % (msg.key(),) from spambayes.storage import NO_TRAINING_FLAG self.removeMessage(msg, observer_flags=NO_TRAINING_FLAG) elif timestamp + self.expireBefore < self.expiry_due: self.expiry_due = timestamp + self.expireBefore class MessageFactory(object): '''Abstract Message Factory''' def create(self, key, content=None): '''Create a message instance''' raise NotImplementedError if __name__ == '__main__': print >> sys.stderr, __doc__ spambayes-1.1a6/spambayes/CostCounter.py0000664000076500000240000001374111112111461020462 0ustar skipstaff00000000000000from spambayes.Options import options class CostCounter: name = "Superclass Cost" def __init__(self): self.total = 0 def spam(self, scr): pass def ham(self, scr): pass def __str__(self): return "%s: $%.4f" % (self.name, self.total) class CompositeCostCounter: def __init__(self, cclist): self.clients = cclist def spam(self, scr): for c in self.clients: c.spam(scr) def ham(self, scr): for c in self.clients: c.ham(scr) def __str__(self): s = [] for c in self.clients: s.append(str(c)) return '\n'.join(s) class DelayedCostCounter(CompositeCostCounter): def __init__(self, cclist): CompositeCostCounter.__init__(self, cclist) self.spamscr = [] self.hamscr = [] def spam(self, scr): self.spamscr.append(scr) def ham(self, scr): self.hamscr.append(scr) def __str__(self): for scr in self.spamscr: CompositeCostCounter.spam(self, scr) for scr in self.hamscr: CompositeCostCounter.ham(self, scr) s = [] for line in CompositeCostCounter.__str__(self).split('\n'): s.append('Delayed-'+line) return '\n'.join(s) class CountCostCounter(CostCounter): def __init__(self): CostCounter.__init__(self) self._fp = 0 self._fn = 0 self._unsure = 0 self._unsureham = 0 self._unsurespam = 0 self._spam = 0 self._ham = 0 self._correctham = 0 self._correctspam = 0 self._total = 0 def spam(self, scr): self._total += 1 self._spam += 1 if scr < options["Categorization", "ham_cutoff"]: self._fn += 1 elif scr < options["Categorization", "spam_cutoff"]: self._unsure += 1 self._unsurespam += 1 else: self._correctspam += 1 def ham(self, scr): self._total += 1 self._ham += 1 if scr > options["Categorization", "spam_cutoff"]: self._fp += 1 elif scr > options["Categorization", "ham_cutoff"]: self._unsure += 1 self._unsureham += 1 else: self._correctham += 1 def __str__(self): return ("Total messages: %d; %d (%.1f%%) ham + %d (%.1f%%) spam\n"%( self._total, self._ham, zd(100.*self._ham,self._total), self._spam, zd(100.*self._spam,self._total))+ "Ham: %d (%.2f%%) ok, %d (%.2f%%) unsure, %d (%.2f%%) fp\n"%( self._correctham, zd(100.*self._correctham,self._ham), self._unsureham, zd(100.*self._unsureham,self._ham), self._fp, zd(100.*self._fp,self._ham))+ "Spam: %d (%.2f%%) ok, %d (%.2f%%) unsure, %d (%.2f%%) fn\n"%( self._correctspam, zd(100.*self._correctspam,self._spam), self._unsurespam, zd(100.*self._unsurespam,self._spam), self._fn, zd(100.*self._fn,self._spam))+ "Score False: %.2f%% Unsure %.2f%%"%( zd(100.*(self._fp+self._fn),self._total), zd(100.*self._unsure,self._total))) def zd(x, y): if y > 0: return x / y else: return 0 class StdCostCounter(CostCounter): name = "Standard Cost" def spam(self, scr): if scr < options["Categorization", "ham_cutoff"]: self.total += options["TestDriver", "best_cutoff_fn_weight"] elif scr < options["Categorization", "spam_cutoff"]: self.total += options["TestDriver", "best_cutoff_unsure_weight"] def ham(self, scr): if scr > options["Categorization", "spam_cutoff"]: self.total += options["TestDriver", "best_cutoff_fp_weight"] elif scr > options["Categorization", "ham_cutoff"]: self.total += options["TestDriver", "best_cutoff_unsure_weight"] class FlexCostCounter(CostCounter): name = "Flex Cost" def _lambda(self, scr): if scr < options["Categorization", "ham_cutoff"]: return 0 elif scr > options["Categorization", "spam_cutoff"]: return 1 else: return (scr - options["Categorization", "ham_cutoff"]) / ( options["Categorization", "spam_cutoff"] \ - options["Categorization", "ham_cutoff"]) def spam(self, scr): self.total += (1 - self._lambda(scr)) * options["TestDriver", "best_cutoff_fn_weight"] def ham(self, scr): self.total += self._lambda(scr) * options["TestDriver", "best_cutoff_fp_weight"] class Flex2CostCounter(FlexCostCounter): name = "Flex**2 Cost" def spam(self, scr): self.total += (1 - self._lambda(scr))**2 * options["TestDriver", "best_cutoff_fn_weight"] def ham(self, scr): self.total += self._lambda(scr)**2 * options["TestDriver", "best_cutoff_fp_weight"] def default(): return CompositeCostCounter([ CountCostCounter(), StdCostCounter(), FlexCostCounter(), Flex2CostCounter(), DelayedCostCounter([ CountCostCounter(), StdCostCounter(), FlexCostCounter(), Flex2CostCounter(), ]) ]) def nodelay(): return CompositeCostCounter([ CountCostCounter(), StdCostCounter(), FlexCostCounter(), Flex2CostCounter(), ]) if __name__ == "__main__": cc = default() cc.ham(0) cc.spam(1) cc.ham(0.5) cc.spam(0.5) options["Categorization", "spam_cutoff"] = 0.7 options["Categorization", "ham_cutoff"] = 0.4 print cc spambayes-1.1a6/spambayes/dbmstorage.py0000775000076500000240000000413511142731512020351 0ustar skipstaff00000000000000"""Wrapper to open an appropriate dbm storage type.""" from spambayes.Options import options import sys import whichdb import os class error(Exception): pass def open_db3hash(*args): """Open a bsddb3 hash.""" import bsddb3 return bsddb3.hashopen(*args) def open_dbhash(*args): """Open a bsddb hash. Don't use this on Windows, unless Python 2.3 or greater is used, in which case bsddb3 is actually named bsddb.""" from spambayes.port import bsddb return bsddb.hashopen(*args) def open_gdbm(*args): """Open a gdbm database.""" from spambayes.port import gdbm if gdbm is not None: return gdbm.open(*args) raise ImportError("gdbm not available") def open_best(*args): if sys.platform == "win32": # Note that Python 2.3 and later ship with the new bsddb interface # as the default bsddb module - so 2.3 can use the old name safely. funcs = [open_db3hash, open_gdbm] if sys.version_info >= (2, 3): funcs.insert(0, open_dbhash) else: funcs = [open_db3hash, open_dbhash, open_gdbm] for f in funcs: try: return f(*args) except ImportError: pass raise error("No dbm modules available!") open_funcs = { "best": open_best, "db3hash": open_db3hash, "dbhash": open_dbhash, "gdbm": open_gdbm, } def open(db_name, mode): if os.path.exists(db_name) and \ options.default("globals", "dbm_type") != \ options["globals", "dbm_type"]: # let the file tell us what db to use dbm_type = whichdb.whichdb(db_name) # if we are using Windows and Python < 2.3, then we need to use # db3hash, not dbhash. if (sys.platform == "win32" and sys.version_info < (2, 3) and dbm_type == "dbhash"): dbm_type = "db3hash" else: # fresh file or overridden - open with what the user specified dbm_type = options["globals", "dbm_type"].lower() f = open_funcs.get(dbm_type) if f is None: raise error("Unknown dbm type: %s" % dbm_type) return f(db_name, mode) spambayes-1.1a6/spambayes/Dibbler.py0000664000076500000240000007647611116632052017603 0ustar skipstaff00000000000000 """ *Introduction* Dibbler is a Python web application framework. It lets you create web-based applications by writing independent plug-in modules that don't require any networking code. Dibbler takes care of the HTTP side of things, leaving you to write the application code. *Plugins and Methlets* Dibbler uses a system of plugins to implement the application logic. Each page maps to a 'methlet', which is a method of a plugin object that serves that page, and is named after the page it serves. The address `http://server/spam` calls the methlet `onSpam`. `onHome` is a reserved methlet name for the home page, `http://server/`. For resources that need a file extension (eg. images) you can use a URL such as `http://server/eggs.gif` to map to the `onEggsGif` methlet. All the registered plugins are searched for the appropriate methlet, so you can combine multiple plugins to build your application. A methlet needs to call `self.writeOKHeaders('text/html')` followed by `self.write(content)`. You can pass whatever content-type you like to `writeOKHeaders`, so serving images, PDFs, etc. is no problem. If a methlet wants to return an HTTP error code, it should call (for example) `self.writeError(403, "Forbidden")` instead of `writeOKHeaders` and `write`. If it wants to write its own headers (for instance to return a redirect) it can simply call `write` with the full HTTP response. If a methlet raises an exception, it is automatically turned into a "500 Server Error" page with a full traceback in it. *Parameters* Methlets can take parameters, the values of which are taken from form parameters submitted by the browser. So if your form says `
    ...` then your methlet should look like `def onSubscribe(self, email=None)`. It's good practice to give all the parameters default values, in case the user navigates to that URL without submitting a form, or submits the form without filling in any parameters. If you have lots of parameters, or their names are determined at runtime, you can define your methlet like this: `def onComplex(self, **params)` to get a dictionary of parameters. *Example* Here's a web application server that serves a calendar for a given year: >>> import Dibbler, calendar >>> class Calendar(Dibbler.HTTPPlugin): ... _form = '''

    Calendar Server

    ... ... Year: ...
    ...
    %s
    ''' ... ... def onHome(self, year=None): ... if year: ... result = calendar.calendar(int(year)) ... else: ... result = "" ... self.writeOKHeaders('text/html') ... self.write(self._form % result) ... >>> httpServer = Dibbler.HTTPServer(8888) >>> httpServer.register(Calendar()) >>> Dibbler.run(launchBrowser=True) Your browser will start, and you can ask for a calendar for the year of your choice. If you don't want to start the browser automatically, just call `run()` with no arguments - the application is available at http://localhost:8888/ . You'll have to kill the server manually because it provides no way to stop it; a real application would have some kind of 'shutdown' methlet that called `sys.exit()`. By combining Dibbler with an HTML manipulation library like PyMeld (shameless plug - see http://entrian.com/PyMeld for details) you can keep the HTML and Python code separate. *Building applications* You can run several plugins together like this: >>> httpServer = Dibbler.HTTPServer() >>> httpServer.register(plugin1, plugin2, plugin3) >>> Dibbler.run() ...so many plugin objects, each implementing a different set of pages, can cooperate to implement a web application. See also the `HTTPServer` documentation for details of how to run multiple `Dibbler` environments simultaneously in different threads. *Controlling connections* There are times when your code needs to be informed the moment an incoming connection is received, before any HTTP conversation begins. For instance, you might want to only accept connections from `localhost` for security reasons. If this is the case, your plugin should implement the `onIncomingConnection` method. This will be passed the incoming socket before any reads or writes have taken place, and should return True to allow the connection through or False to reject it. Here's an implementation of the `localhost`-only idea: >>> def onIncomingConnection(self, clientSocket): >>> return clientSocket.getpeername()[0] == clientSocket.getsockname()[0] *Advanced usage: Dibbler Contexts* If you want to run several independent Dibbler environments (in different threads for example) then each should use its own `Context`. Normally you'd say something like: >>> httpServer = Dibbler.HTTPServer() >>> httpServer.register(MyPlugin()) >>> Dibbler.run() but that's only safe to do from one thread. Instead, you can say: >>> myContext = Dibbler.Context() >>> httpServer = Dibbler.HTTPServer(context=myContext) >>> httpServer.register(MyPlugin()) >>> Dibbler.run(myContext) in as many threads as you like. *Dibbler and asyncore* If this section means nothing to you, you can safely ignore it. Dibbler is built on top of Python's asyncore library, which means that it integrates into other asyncore-based applications, and you can write other asyncore-based components and run them as part of the same application. By default, Dibbler uses the default asyncore socket map. This means that `Dibbler.run()` also runs your asyncore-based components, provided they're using the default socket map. If you want to tell Dibbler to use a different socket map, either to co-exist with other asyncore-based components using that map or to insulate Dibbler from such components by using a different map, you need to use a `Dibbler.Context`. If you're using your own socket map, give it to the context: `context = Dibbler.Context(myMap)`. If you want Dibbler to use its own map: `context = Dibbler.Context({})`. You can either call `Dibbler.run(context)` to run the async loop, or call `asyncore.loop()` directly - the only difference is that the former has a few more options, like launching the web browser automatically. *Self-test* Running `Dibbler.py` directly as a script runs the example calendar server plus a self-test. """ # Dibbler is released under the Python Software Foundation license; see # http://www.python.org/ __author__ = "Richie Hindle " __credits__ = "Tim Stone" try: import cStringIO as StringIO except ImportError: import StringIO import sys, re, time, traceback, base64 import socket, asyncore, asynchat, cgi, urlparse, webbrowser try: "".rstrip("abc") except TypeError: # rstrip(chars) requires Python 2.2.2 or higher. Apart from that # we probably work with Python 2.2 (and say we do), so provide the # ability to do this for that case. RSTRIP_CHARS_AVAILABLE = False else: RSTRIP_CHARS_AVAILABLE = True from spambayes.port import md5 class BrighterAsyncChat(asynchat.async_chat): """An asynchat.async_chat that doesn't give spurious warnings on receiving an incoming connection, lets SystemExit cause an exit, can flush its output, and will correctly remove itself from a non-default socket map on `close()`.""" def __init__(self, conn=None, map=None): """See `asynchat.async_chat`.""" asynchat.async_chat.__init__(self, conn) self.__map = map self._closed = False def handle_connect(self): """Suppresses the asyncore "unhandled connect event" warning.""" pass def handle_error(self): """Let SystemExit cause an exit.""" type, v, t = sys.exc_info() if type == SystemExit: raise else: asynchat.async_chat.handle_error(self) def flush(self): """Flush everything in the output buffer.""" # We check self._closed here because of the case where # self.initiate_send() raises an exception, causing self.close() # to be called. If we didn't check, we could end up in an infinite # loop. while (self.producer_fifo or self.ac_out_buffer) and not self._closed: self.initiate_send() def close(self): """Remove this object from the correct socket map.""" self._closed = True self.del_channel(self.__map) self.socket.close() class Context: """See the main documentation for details of `Dibbler.Context`.""" def __init__(self, asyncMap=asyncore.socket_map): self._HTTPPort = None # Stores the port for `run(launchBrowser=True)` self._map = asyncMap def pop(self, key): return self._map.pop(key) def keys(self): return self._map.keys() def __len__(self): return len(self._map) _defaultContext = Context() class Listener(asyncore.dispatcher): """Generic listener class used by all the different types of server. Listens for incoming socket connections and calls a factory function to create handlers for them.""" def __init__(self, port, factory, factoryArgs, socketMap=_defaultContext._map): """Creates a listener object, which will listen for incoming connections when Dibbler.run is called: o port: The TCP/IP (address, port) to listen on. Usually '' - meaning bind to all IP addresses that the machine has - will be passed as the address. If `port` is just an int, an address of '' will be assumed. o factory: The function to call to create a handler (can be a class name). o factoryArgs: The arguments to pass to the handler factory. For proper context support, this should include a `context` argument (or a `socketMap` argument for pure asyncore listeners). The incoming socket will be prepended to this list, and passed as the first argument. See `HTTPServer` for an example. o socketMap: Optional. The asyncore socket map to use. If you're using a `Dibbler.Context`, pass context._map. See `HTTPServer` for an example `Listener` - it's a good deal smaller than this description!""" asyncore.dispatcher.__init__(self, map=socketMap) self.socketMap = socketMap self.factory = factory self.factoryArgs = factoryArgs s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setblocking(False) self.set_socket(s, self.socketMap) self.set_reuse_addr() if type(port) != type(()): port = ('', port) try: self.bind(port) except socket.error: print >> sys.stderr, "port", port, "in use" raise self.listen(5) def handle_accept(self): """Asyncore override.""" # If an incoming connection is instantly reset, eg. by following a # link in the web interface then instantly following another one or # hitting stop, handle_accept() will be triggered but accept() will # return None. result = self.accept() if result: clientSocket, clientAddress = result args = [clientSocket] + list(self.factoryArgs) self.factory(*args) class HTTPServer(Listener): """A web server with which you can register `HTTPPlugin`s to serve up your content - see `HTTPPlugin` for detailed documentation and examples. `port` specifies the TCP/IP (address, port) on which to run, defaulting to ('', 80). `context` optionally specifies a `Dibbler.Context` for the server. """ NO_AUTHENTICATION = "None" BASIC_AUTHENTICATION = "Basic" DIGEST_AUTHENTICATION = "Digest" def __init__(self, port=('', 80), context=_defaultContext): """Create an `HTTPServer` for the given port.""" Listener.__init__(self, port, _HTTPHandler, (self, context), context._map) self._plugins = [] try: context._HTTPPort = port[1] except TypeError: context._HTTPPort = port def register(self, *plugins): """Registers one or more `HTTPPlugin`-derived objects with the server.""" for plugin in plugins: self._plugins.append(plugin) def requestAuthenticationMode(self): """Override: HTTP Authentication. It should return a value among NO_AUTHENTICATION, BASIC_AUTHENTICATION and DIGEST_AUTHENTICATION. The two last values will force HTTP authentication respectively through Base64 and MD5 encodings.""" return self.NO_AUTHENTICATION def isValidUser(self, name, password): """Override: Return True for authorized logins.""" return True def getPasswordForUser(self, name): """Override: Return the password associated to the specified user name.""" return '' def getRealm(self): """Override: Specify the HTTP authentication realm.""" return "Dibbler application server" def getCancelMessage(self): """Override: Specify the cancel message for an HTTP Authentication.""" return "You must log in." class _HTTPHandler(BrighterAsyncChat): """This is a helper for the HTTP server class - one of these is created for each incoming request, and does the job of decoding the HTTP traffic and driving the plugins.""" # RE to extract option="value" fields from # digest auth login field _login_splitter = re.compile('([a-zA-Z]+)=(".*?"|.*?),?') def __init__(self, clientSocket, server, context): # Grumble: asynchat.__init__ doesn't take a 'map' argument, # hence the two-stage construction. BrighterAsyncChat.__init__(self, map=context._map) BrighterAsyncChat.set_socket(self, clientSocket, context._map) self._context = context self._server = server self._request = '' self.set_terminator('\r\n\r\n') # Because a methlet is likely to call `writeOKHeaders` before doing # anything else, an unexpected exception won't send back a 500, which # is poor. So we buffer any sent headers until either a plain `write` # happens or the methlet returns. self._bufferedHeaders = [] self._headersWritten = False # Tell the plugins about the connection, letting them veto it. for plugin in self._server._plugins: if not plugin.onIncomingConnection(clientSocket): self.close() def collect_incoming_data(self, data): """Asynchat override.""" self._request = self._request + data def found_terminator(self): """Asynchat override.""" # Parse the HTTP request. requestLine, headers = (self._request+'\r\n').split('\r\n', 1) try: method, url, version = requestLine.strip().split() except ValueError: self.writeError(400, "Malformed request: '%s'" % requestLine) self.close_when_done() return # Parse the URL, and deal with POST vs. GET requests. method = method.upper() unused, unused, path, unused, query, unused = urlparse.urlparse(url) cgiParams = cgi.parse_qs(query, keep_blank_values=True) if self.get_terminator() == '\r\n\r\n' and method == 'POST': # We need to read the body - set a numeric async_chat terminator # equal to the Content-Length. match = re.search(r'(?i)content-length:\s*(\d+)', headers) contentLength = int(match.group(1)) if contentLength > 0: self.set_terminator(contentLength) self._request = self._request + '\r\n\r\n' return # Have we just read the body of a POSTed request? Decode the body, # which will contain parameters and possibly uploaded files. if type(self.get_terminator()) is type(1): self.set_terminator('\r\n\r\n') body = self._request.split('\r\n\r\n', 1)[1] match = re.search(r'(?i)content-type:\s*([^\r\n]+)', headers) contentTypeHeader = match.group(1) contentType, pdict = cgi.parse_header(contentTypeHeader) if contentType == 'multipart/form-data': # multipart/form-data - probably a file upload. bodyFile = StringIO.StringIO(body) cgiParams.update(cgi.parse_multipart(bodyFile, pdict)) else: # A normal x-www-form-urlencoded. cgiParams.update(cgi.parse_qs(body, keep_blank_values=True)) # Convert the cgi params into a simple dictionary. params = {} for name, value in cgiParams.iteritems(): params[name] = value[0] # Parse the headers. headersRegex = re.compile('([^:]*):\s*(.*)') headersDict = dict([headersRegex.match(line).groups(2) for line in headers.split('\r\n') if headersRegex.match(line)]) # HTTP Basic/Digest Authentication support. serverAuthMode = self._server.requestAuthenticationMode() if serverAuthMode != HTTPServer.NO_AUTHENTICATION: # The server wants us to authenticate the user. authResult = False authHeader = headersDict.get('Authorization') if authHeader: authMatch = re.search('(\w+)\s+(.*)', authHeader) authenticationMode, login = authMatch.groups() if authenticationMode == HTTPServer.BASIC_AUTHENTICATION: authResult = self._basicAuthentication(login) elif authenticationMode == HTTPServer.DIGEST_AUTHENTICATION: authResult = self._digestAuthentication(login, method) else: print >> sys.stderr, "Unknown mode: %s" % authenticationMode if not authResult: self.writeUnauthorizedAccess(serverAuthMode) # Find and call the methlet. '/eggs.gif' becomes 'onEggsGif'. if path == '/': path = '/Home' pieces = path[1:].split('.') name = 'on' + ''.join([piece.capitalize() for piece in pieces]) for plugin in self._server._plugins: if hasattr(plugin, name): # The plugin's APIs (`write`, etc) reflect back to us via # `plugin._handler`. plugin._handler = self try: # Call the methlet. getattr(plugin, name)(**params) if self._bufferedHeaders: # The methlet returned without writing anything other # than headers. This isn't unreasonable - it might # have written a 302 or something. Flush the buffered # headers self.write(None) except: # The methlet raised an exception - send the traceback to # the browser, unless it's SystemExit in which case we let # it go. eType, eValue, eTrace = sys.exc_info() if eType == SystemExit: # Close all the listeners so that no further incoming # connections appear. contextMap = self._context._map for dispatcher in contextMap.values(): if isinstance(dispatcher, Listener): dispatcher.close() # Let any existing connections close down first. This # has happened when all we have left are _HTTPHandlers # (this one plus any others that are using keep-alive; # none of the others can be actually doing any work # because *we're* the one doing the work). def isProtected(dispatcher): return not isinstance(dispatcher, _HTTPHandler) while len(filter(isProtected, contextMap.values())) > 0: asyncore.poll(timeout=1, map=contextMap) raise SystemExit message = """

    500 Server error

    %s
    """ details = traceback.format_exception(eType, eValue, eTrace) details = '\n'.join(details) self.writeError(500, message % cgi.escape(details)) plugin._handler = None break else: self.onUnknown(path, params) # `close_when_done` and `Connection: close` ensure that we don't # support keep-alives or pipelining. There are problems with some # browsers, for instance with extra characters being appended after # the body of a POSTed request. self.close_when_done() def onUnknown(self, path, params): """Handler for unknown URLs. Returns a 404 page.""" self.writeError(404, "Not found: '%s'" % path) def writeOKHeaders(self, contentType, extraHeaders={}): """Reflected from `HTTPPlugin`s.""" # Buffer the headers until there's a `write`, in case an error occurs. timeNow = time.gmtime(time.time()) httpNow = time.strftime('%a, %d %b %Y %H:%M:%S GMT', timeNow) headers = [] headers.append("HTTP/1.1 200 OK") headers.append("Connection: close") headers.append('Content-Type: %s; charset="utf-8"' % contentType) headers.append("Date: %s" % httpNow) for name, value in extraHeaders.items(): headers.append("%s: %s" % (name, value)) headers.append("") headers.append("") self._bufferedHeaders = headers def writeError(self, code, message): """Reflected from `HTTPPlugin`s.""" # Writing an error overrides any buffered headers, but obviously # doesn't want to write any headers if some have already gone. headers = [] if not self._headersWritten: headers.append("HTTP/1.0 %d Error" % code) headers.append("Connection: close") headers.append('Content-Type: text/html; charset="utf-8"') headers.append("") headers.append("") self.push("%s%s" % \ ('\r\n'.join(headers), message)) def write(self, content): """Reflected from `HTTPPlugin`s.""" # The methlet is writing, so write any buffered headers first. headers = [] if self._bufferedHeaders: headers = self._bufferedHeaders self._bufferedHeaders = None self._headersWritten = True # `write(None)` just flushes buffered headers. if content is None: content = '' self.push('\r\n'.join(headers) + str(content)) def writeUnauthorizedAccess(self, authenticationMode): """Access is protected by HTTP authentication.""" if authenticationMode == HTTPServer.BASIC_AUTHENTICATION: authString = self._getBasicAuthString() elif authenticationMode == HTTPServer.DIGEST_AUTHENTICATION: authString = self._getDigestAuthString() else: self.writeError(500, "Inconsistent authentication mode.") return headers = [] headers.append('HTTP/1.0 401 Unauthorized') headers.append('WWW-Authenticate: ' + authString) headers.append('Connection: close') headers.append('Content-Type: text/html; charset="utf-8"') headers.append('') headers.append('') self.write('\r\n'.join(headers) + self._server.getCancelMessage()) self.close_when_done() def _getDigestAuthString(self): """Builds the WWW-Authenticate header for Digest authentication.""" authString = 'Digest realm="' + self._server.getRealm() + '"' authString += ', nonce="' + self._getCurrentNonce() + '"' authString += ', opaque="0000000000000000"' authString += ', stale="false"' authString += ', algorithm="MD5"' authString += ', qop="auth"' return authString def _getBasicAuthString(self): """Builds the WWW-Authenticate header for Basic authentication.""" return 'Basic realm="' + self._server.getRealm() + '"' def _getCurrentNonce(self): """Returns the current nonce value. This value is a Base64 encoding of current time plus 20 minutes. This means the nonce will expire 20 minutes from now.""" timeString = time.asctime(time.localtime(time.time() + 20*60)) if RSTRIP_CHARS_AVAILABLE: return base64.encodestring(timeString).rstrip('\n=') else: # Python pre 2.2.2, so can't do a rstrip(chars). Do it # manually instead. def rstrip(s, chars): if not s: return s if s[-1] in chars: return rstrip(s[:-1]) return s return rstrip(base64.encodestring(timeString), '\n=') def _isValidNonce(self, nonce): """Check if the specified nonce is still valid. A nonce is invalid when its time converted value is lower than current time.""" padAmount = len(nonce) % 4 if padAmount > 0: padAmount = 4 - padAmount nonce += '=' * (len(nonce) + padAmount) decoded = base64.decodestring(nonce) return time.time() < time.mktime(time.strptime(decoded)) def _basicAuthentication(self, login): """Performs a Basic HTTP authentication. Returns True when the user has logged in successfully, False otherwise.""" userName, password = base64.decodestring(login).split(':') return self._server.isValidUser(userName, password) def _digestAuthentication(self, login, method): """Performs a Digest HTTP authentication. Returns True when the user has logged in successfully, False otherwise.""" def stripQuotes(s): return (s[0] == '"' and s[-1] == '"') and s[1:-1] or s options = dict(self._login_splitter.findall(login)) userName = stripQuotes(options["username"]) password = self._server.getPasswordForUser(userName) nonce = stripQuotes(options["nonce"]) # The following computations are based upon RFC 2617. A1 = "%s:%s:%s" % (userName, self._server.getRealm(), password) HA1 = md5(A1).hexdigest() A2 = "%s:%s" % (method, stripQuotes(options["uri"])) HA2 = md5(A2).hexdigest() unhashedDigest = "" if options.has_key("qop"): # IE 6.0 doesn't give nc back correctly? if not options["nc"]: options["nc"] = "00000001" # Firefox 1.0 doesn't give qop back correctly? if not options["qop"]: options["qop"] = "auth" unhashedDigest = "%s:%s:%s:%s:%s:%s" % \ (HA1, nonce, stripQuotes(options["nc"]), stripQuotes(options["cnonce"]), stripQuotes(options["qop"]), HA2) else: unhashedDigest = "%s:%s:%s" % (HA1, nonce, HA2) hashedDigest = md5(unhashedDigest).hexdigest() return (stripQuotes(options["response"]) == hashedDigest and self._isValidNonce(nonce)) class HTTPPlugin: """Base class for HTTP server plugins. See the main documentation for details.""" def __init__(self): # self._handler is filled in by `HTTPHandler.found_terminator()`. pass def onIncomingConnection(self, clientSocket): """Implement this and return False to veto incoming connections.""" return True def writeOKHeaders(self, contentType, extraHeaders={}): """A methlet should call this with the Content-Type and optionally a dictionary of extra headers (eg. Expires) before calling `write()`.""" return self._handler.writeOKHeaders(contentType, extraHeaders) def writeError(self, code, message): """A methlet should call this instead of `writeOKHeaders()` / `write()` to report an HTTP error (eg. 403 Forbidden).""" return self._handler.writeError(code, message) def write(self, content): """A methlet should call this after `writeOKHeaders` to write the page's content.""" return self._handler.write(content) def flush(self): """A methlet can call this after calling `write`, to ensure that the content is written immediately to the browser. This isn't necessary most of the time, but if you're writing "Please wait..." before performing a long operation, calling `flush()` is a good idea.""" return self._handler.flush() def close(self, flush=True): """Closes the connection to the browser. You should call `close()` before calling `sys.exit()` in any 'shutdown' methlets you write.""" if flush: self.flush() return self._handler.close() def run(launchBrowser=False, context=_defaultContext): """Runs a `Dibbler` application. Servers listen for incoming connections and route requests through to plugins until a plugin calls `sys.exit()` or raises a `SystemExit` exception.""" if launchBrowser: try: url = "http://localhost:%d/" % context._HTTPPort webbrowser.open_new(url) except webbrowser.Error, e: print "\n%s.\nPlease point your web browser at %s." % (e, url) asyncore.loop(map=context._map) def runTestServer(readyEvent=None): """Runs the calendar server example, with an added `/shutdown` URL.""" import calendar class Calendar(HTTPPlugin): _form = '''

    Calendar Server

    Year:
    %s
    ''' def onHome(self, year=None): if year: result = calendar.calendar(int(year)) else: result = "" self.writeOKHeaders('text/html') self.write(self._form % result) def onShutdown(self): self.writeOKHeaders('text/html') self.write("

    OK.

    ") self.close() sys.exit() httpServer = HTTPServer(8888) httpServer.register(Calendar()) if readyEvent: # Tell the self-test code that the test server is up and running. readyEvent.set() run(launchBrowser=True) def test(): """Run a self-test.""" # Run the calendar server in a separate thread. import threading, urllib testServerReady = threading.Event() threading.Thread(target=runTestServer, args=(testServerReady,)).start() testServerReady.wait() # Connect to the server and ask for a calendar. page = urllib.urlopen("http://localhost:8888/?year=2003").read() if page.find('January') != -1: print "Self test passed." else: print "Self-test failed!" # Wait for a key while the user plays with his browser. raw_input("Press any key to shut down the application server...") # Ask the server to shut down. page = urllib.urlopen("http://localhost:8888/shutdown").read() if page.find('OK') != -1: print "Shutdown OK." else: print "Shutdown failed!" if __name__ == '__main__': test() spambayes-1.1a6/spambayes/dnscache.py0000664000076500000000000003061411217221400017754 0ustar skipwheel00000000000000# Copyright 2004, Matthew Dixon Cowles . # Distributable under the same terms as the Python programming language. # Inspired by the KevinL's cache included with PyDNS. # Provided with NO WARRANTY. # Version 0.1 2004 06 27 # Version 0.11 2004 07 06 Fixed zero division error in __del__ # From http://sourceforge.net/projects/pydns/ import DNS import sys import os import operator import time import types import socket from spambayes.Options import options from spambayes.safepickle import pickle_read, pickle_write kCheckForPruneEvery = 20 kMaxTTL = 60 * 60 * 24 * 7 # One week # Some servers always return a TTL of zero. We'll hold onto data a bit # longer. kMinTTL = 24 * 60 * 60 * 1 # one day kPruneThreshold = 5000 # May go over slightly; numbers chosen at random kPruneDownTo = 2500 class lookupResult(object): #__slots__=("qType","answer","question","expiresAt","lastUsed") def __init__(self, qType, answer, question, expiresAt, now): self.qType = qType self.answer = answer self.question = question self.expiresAt = expiresAt self.lastUsed = now return None # From ActiveState's Python cookbook # Yakov Markovitch, Fast sort the list of objects by object's attribute # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52230 def sort_by_attr(seq, attr): """Sort the sequence of objects by object's attribute Arguments: seq - the list or any sequence (including immutable one) of objects to sort. attr - the name of attribute to sort by Returns: the sorted list of objects. """ #import operator # Use the "Schwartzian transform" # Create the auxiliary list of tuples where every i-th tuple has form # (seq[i].attr, i, seq[i]) and sort it. The second item of tuple is needed not # only to provide stable sorting, but mainly to eliminate comparison of objects # (which can be expensive or prohibited) in case of equal attribute values. intermed = map(None, map(getattr, seq, (attr,)*len(seq)), xrange(len(seq)), seq) intermed.sort() return map(operator.getitem, intermed, (-1,) * len(intermed)) class cache: def __init__(self, dnsServer=None, cachefile=""): # These attributes intended for user setting self.printStatsAtEnd = False # As far as I can tell from the standards, # it's legal to have more than one PTR record # for an address. That is, it's legal to get # more than one name back when you do a # reverse lookup on an IP address. I don't # know of a use for that and I've never seen # it done. And I don't think that most # people would expect it. So forward ("A") # lookups always return a list. Reverse # ("PTR") lookups return a single name unless # this attribute is set to False. self.returnSinglePTR = True # How long to cache an error as no data self.cacheErrorSecs=5*60 # How long to wait for the server self.dnsTimeout=10 # end of user-settable attributes self.cachefile = os.path.expanduser(cachefile) self.caches = None if self.cachefile and os.path.exists(self.cachefile): try: self.caches = pickle_read(self.cachefile) except: os.unlink(self.cachefile) if self.caches is None: self.caches = {"A": {}, "PTR": {}} if options["globals", "verbose"]: if self.caches["A"] or self.caches["PTR"]: print >> sys.stderr, "opened existing cache with", print >> sys.stderr, len(self.caches["A"]), "A records", print >> sys.stderr, "and", len(self.caches["PTR"]), print >> sys.stderr, "PTR records" else: print >> sys.stderr, "opened new cache" self.hits=0 # These two for statistics self.misses=0 self.pruneTicker=0 if dnsServer == None: DNS.DiscoverNameServers() self.queryObj = DNS.DnsRequest() else: self.queryObj = DNS.DnsRequest(server=dnsServer) return None def close(self): if self.printStatsAtEnd: self.printStats() if self.cachefile: pickle_write(self.cachefile, self.caches) def printStats(self): for key,val in self.caches.items(): totAnswers=0 for item in val.values(): totAnswers+=len(item) print >> sys.stderr, "cache", key, "has", len(self.caches[key]), print >> sys.stderr, "question(s) and", totAnswers, "answer(s)" if self.hits+self.misses == 0: print >> sys.stderr, "No queries" else: print >> sys.stderr, self.hits, "hits,", self.misses, "misses", print >> sys.stderr, "(%.1f%% hits)" % \ (self.hits/float(self.hits+self.misses)*100) def prune(self, now): # I want this to be as fast as reasonably possible. # If I didn't, I'd probably do various things differently # Is there a faster way to do this? allAnswers = [] for cache in self.caches.values(): for val in cache.values(): allAnswers += val allAnswers = sort_by_attr(allAnswers,"expiresAt") allAnswers.reverse() while True: if allAnswers[-1].expiresAt > now: break answer = allAnswers.pop() c = self.caches[answer.qType] c[answer.question].remove(answer) if not c[answer.question]: del c[answer.question] if options["globals", "verbose"]: self.printStats() if len(allAnswers)<=kPruneDownTo: return None # Expiring didn't get us down to the size we want, so delete # some entries least-recently-used-wise. I'm not by any means # sure that this is the best strategy, but as yet I don't have # data to test different strategies. allAnswers = sort_by_attr(allAnswers, "lastUsed") allAnswers.reverse() numToDelete = len(allAnswers)-kPruneDownTo for _count in xrange(numToDelete): answer = allAnswers.pop() c = self.caches[answer.qType] c[answer.question].remove(answer) if not c[answer.question]: del c[answer.question] return None def formatForReturn(self, listOfObjs): if len(listOfObjs) == 1 and listOfObjs[0].answer == None: return [] if listOfObjs[0].qType == "PTR" and self.returnSinglePTR: return listOfObjs[0].answer return [ obj.answer for obj in listOfObjs ] def lookup(self,question,qType="A"): qType = qType.upper() if qType not in ("A","PTR"): raise ValueError,"Query type must be one of A, PTR" now = int(time.time()) # Finding the len() of a dictionary isn't an expensive operation # but doing it twice for every lookup isn't necessary. self.pruneTicker += 1 if self.pruneTicker == kCheckForPruneEvery: self.pruneTicker = 0 if len(self.caches["A"])+len(self.caches["PTR"])>kPruneThreshold: self.prune(now) cacheToLookIn = self.caches[qType] try: answers = cacheToLookIn[question] except KeyError: pass else: if answers: ind = 0 # No guarantee that expire has already been done while ind> sys.stderr, "lookup failure:", question if not answers: del cacheToLookIn[question] else: self.hits += 1 return self.formatForReturn(answers) # Not in cache or we just expired it self.misses += 1 if qType == "PTR": qList = question.split(".") qList.reverse() queryQuestion = ".".join(qList)+".in-addr.arpa" else: queryQuestion = question # where do we get NXDOMAIN? try: reply = self.queryObj.req(queryQuestion, qtype=qType, timeout=self.dnsTimeout) except DNS.Base.DNSError,detail: if detail.args[0] not in ("Timeout", "nothing to lookup"): print >> sys.stderr, detail.args[0] print >> sys.stderr, "Error, fixme", detail print >> sys.stderr, "Question was", queryQuestion print >> sys.stderr, "Original question was", question print >> sys.stderr, "Type was", qType objs = [lookupResult(qType, None, question, self.cacheErrorSecs+now, now)] cacheToLookIn[question] = objs # Add to format for return? return self.formatForReturn(objs) except socket.gaierror,detail: print >> sys.stderr, "DNS connection failure:", self.queryObj.ns, detail print >> sys.stderr, "Defaults:", DNS.defaults objs = [] for answer in reply.answers: if answer["typename"] == qType: # PyDNS returns TTLs as longs but RFC 1035 says that the TTL # value is a signed 32-bit value and must be positive, so it # should be safe to coerce it to a Python integer. And # anyone who sets a time to live of more than 2^31-1 seconds # (68 years and change) is drunk. Arguably, I ought to # impose a maximum rather than continuing with longs # (int(long) returns long in recent versions of Python). ttl = max(min(int(answer["ttl"]), kMaxTTL), kMinTTL) # RFC 2308 says that you should cache an NXDOMAIN for the # minimum of the minimum field of the SOA record and the TTL # of the SOA. if ttl > 0: item = lookupResult(qType, answer["data"], question, ttl+now, now) objs.append(item) if objs: cacheToLookIn[question] = objs return self.formatForReturn(objs) # Probably SERVFAIL or the like if not reply.authority: objs = [lookupResult(qType, None, question, self.cacheErrorSecs+now, now)] cacheToLookIn[question] = objs return self.formatForReturn(objs) # No such host # # I don't know in what circumstances you'd have more than one authority, # so I'll just assume that the first is what we want. # # RFC 2308 specifies that this how to decide how long to cache an # NXDOMAIN. auth = reply.authority[0] auTTL = int(auth["ttl"]) for item in auth["data"]: if type(item) == types.TupleType and item[0] == "minimum": auMin = int(item[1]) cacheNeg = min(auMin,auTTL) break else: cacheNeg = auTTL objs = [lookupResult(qType, None, question, cacheNeg+now, now)] cacheToLookIn[question] = objs return self.formatForReturn(objs) def main(): import transaction c = cache(cachefile=os.path.expanduser("~/.dnscache")) c.printStatsAtEnd = True for host in ["www.python.org", "www.timsbloggers.com", "www.seeputofor.com", "www.completegarbage.tv", "www.tradelinkllc.com"]: print >> sys.stderr, "checking", host now = time.time() ips = c.lookup(host) print >> sys.stderr, ips, time.time()-now now = time.time() ips = c.lookup(host) print >> sys.stderr, ips, time.time()-now if ips: ip = ips[0] now = time.time() name = c.lookup(ip, qType="PTR") print >> sys.stderr, name, time.time()-now now = time.time() name = c.lookup(ip, qType="PTR") print >> sys.stderr, name, time.time()-now else: print >> sys.stderr, "unknown" c.close() return None if __name__ == "__main__": main() spambayes-1.1a6/spambayes/FileCorpus.py0000664000076500000240000002732711112111743020275 0ustar skipstaff00000000000000#! /usr/bin/env python """FileCorpus.py - Corpus composed of file system artifacts Classes: FileCorpus - an observable dictionary of FileMessages ExpiryFileCorpus - a FileCorpus of young files FileMessage - a subject of Spambayes training FileMessageFactory - a factory to create FileMessage objects GzipFileMessage - A FileMessage zipped for less storage GzipFileMessageFactory - factory to create GzipFileMessage objects Abstract: These classes are concrete implementations of the Corpus framework. FileCorpus is designed to manage corpora that are directories of message files. ExpiryFileCorpus is an ExpiryCorpus of file messages. FileMessage manages messages that are files in the file system. FileMessageFactory is responsible for the creation of FileMessages, in response to requests to a corpus for messages. GzipFileMessage and GzipFileMessageFactory are used to persist messages as zipped files. This can save a bit of persistent storage, though the ability of the compresser to do very much deflation is limited due to the relatively small size of the average textual message. Still, for a large corpus, this could amount to a significant space savings. See Corpus.__doc__ for more information. To Do: o Suggestions? """ # This module is part of the spambayes project, which is Copyright 2002-2007 # The Python Software Foundation and is covered by the Python Software # Foundation license. from __future__ import generators __author__ = "Tim Stone " __credits__ = "Richie Hindle, Tim Peters, all the spambayes contributors." import email from spambayes import Corpus from spambayes import message import os, gzip, fnmatch, time, stat from spambayes.Options import options class FileCorpus(Corpus.Corpus): def __init__(self, factory, directory, filter='*', cacheSize=250): '''Constructor(FileMessageFactory, corpus directory name, fnmatch filter''' Corpus.Corpus.__init__(self, factory, cacheSize) self.directory = directory self.filter = filter # This assumes that the directory exists. A horrible death occurs # otherwise. We *could* simply create it, but that will likely only # mask errors # This will not pick up any changes to the corpus that are made # through the file system. The key list is established in __init__, # and if anybody stores files in the directory, even if they match # the filter, they won't make it into the key list. The same # problem exists if anybody removes files. This *could* be a problem. # If so, we can maybe override the keys() method to account for this, # but there would be training side-effects... The short of it is that # corpora that are managed by FileCorpus should *only* be managed by # FileCorpus (at least for now). External changes that must be made # to the corpus should for the moment be handled by a complete # retraining. for filename in os.listdir(directory): if fnmatch.fnmatch(filename, filter): self.msgs[filename] = None def makeMessage(self, key, content=None): '''Ask our factory to make a Message''' msg = self.factory.create(key, self.directory, content) return msg def addMessage(self, message, observer_flags=0): '''Add a Message to this corpus''' if not fnmatch.fnmatch(message.key(), self.filter): raise ValueError if options["globals", "verbose"]: print 'adding', message.key(), 'to corpus' message.directory = self.directory message.store() # superclass processing *MUST* be done # perform superclass processing *LAST!* Corpus.Corpus.addMessage(self, message, observer_flags) def removeMessage(self, message, observer_flags=0): '''Remove a Message from this corpus''' if options["globals", "verbose"]: print 'removing', message.key(), 'from corpus' message.remove() # superclass processing *MUST* be done # perform superclass processing *LAST!* Corpus.Corpus.removeMessage(self, message, observer_flags) def __repr__(self): '''Instance as a representative string''' nummsgs = len(self.msgs) if nummsgs != 1: s = 's' else: s = '' if options["globals", "verbose"] and nummsgs > 0: lst = ', ' + '%s' % (self.keys()) else: lst = '' return "<%s object at %8.8x, directory: %s, %s message%s%s>" % \ (self.__class__.__name__, \ id(self), \ self.directory, \ nummsgs, s, lst) class ExpiryFileCorpus(Corpus.ExpiryCorpus, FileCorpus): '''FileCorpus of "young" file system artifacts''' def __init__(self, expireBefore, factory, directory, filter='*', cacheSize=250): '''Constructor(FileMessageFactory, corpus directory name, fnmatch filter''' Corpus.ExpiryCorpus.__init__(self, expireBefore) FileCorpus.__init__(self, factory, directory, filter, cacheSize) class FileMessage(object): '''Message that persists as a file system artifact.''' message_class = message.SBHeaderMessage def __init__(self, file_name=None, directory=None): '''Constructor(message file name, corpus directory name)''' self.file_name = file_name self.directory = directory self.loaded = False self._msg = self.message_class() def __getattr__(self, att): """Pretend we are a subclass of message.SBHeaderMessage.""" if hasattr(self, "_msg") and hasattr(self._msg, att): return getattr(self._msg, att) raise AttributeError() def __getitem__(self, k): """Pretend we are a subclass of message.SBHeaderMessage.""" if hasattr(self, "_msg"): return self._msg[k] raise TypeError() def __setitem__(self, k, v): """Pretend we are a subclass of message.SBHeaderMessage.""" if hasattr(self, "_msg"): self._msg[k] = v return raise TypeError() def as_string(self, unixfrom=False): self.load() # ensure that the substance is loaded return self._msg.as_string(unixfrom) def pathname(self): '''Derive the pathname of the message file''' assert self.file_name is not None, \ "Must set filename before using FileMessage instances." assert self.directory is not None, \ "Must set directory before using FileMessage instances." return os.path.join(self.directory, self.file_name) def load(self): '''Read the Message substance from the file''' # This is a tricky one! Some people might have a combination # of gzip and non-gzip messages, especially when they first # change to or from gzip. They should be able to see (but # not create) either type, so a FileMessage load needs to be # able to load gzip messages, even though it is a FileMessage # subclass (GzipFileMessage) that adds the ability to store # messages gzipped. If someone can think of a classier (pun # intended) way of doing this, be my guest. if self.loaded: return assert self.file_name is not None, \ "Must set filename before using FileMessage instances." if options["globals", "verbose"]: print 'loading', self.file_name pn = self.pathname() fp = gzip.open(pn, 'rb') try: self._msg = email.message_from_string(\ fp.read(), _class = self.message_class) except IOError, e: if str(e) == 'Not a gzipped file' or \ str(e) == 'Unknown compression method': # We've probably got both gzipped messages and # non-gzipped messages, and need to work with both. fp.close() fp = open(self.pathname(), 'rb') self._msg = email.message_from_string(\ fp.read(), _class = self.message_class) fp.close() else: # Don't shadow other errors. raise else: fp.close() self.loaded = True def store(self): '''Write the Message substance to the file''' assert self.file_name is not None, \ "Must set filename before using FileMessage instances." if options["globals", "verbose"]: print 'storing', self.file_name fp = open(self.pathname(), 'wb') fp.write(self.as_string()) fp.close() def remove(self): '''Message hara-kiri''' if options["globals", "verbose"]: print 'physically deleting file', self.pathname() try: os.unlink(self.pathname()) except OSError: # The file probably isn't there anymore. Maybe a virus # protection program got there first? if options["globals", "verbose"]: print 'file', self.pathname(), 'can not be deleted' def name(self): '''A unique name for the message''' assert self.file_name is not None, \ "Must set filename before using FileMessage instances." return self.file_name def key(self): '''The key of this message in the msgs dictionary''' assert self.file_name is not None, \ "Must set filename before using FileMessage instances." return self.file_name def __repr__(self): '''Instance as a representative string''' sub = self.as_string() if not options["globals", "verbose"]: if len(sub) > 20: if len(sub) > 40: sub = sub[:20] + '...' + sub[-20:] else: sub = sub[:20] return "<%s object at %8.8x, file: %s, %s>" % \ (self.__class__.__name__, \ id(self), self.pathname(), sub) def __str__(self): '''Instance as a printable string''' return self.__repr__() def createTimestamp(self): '''Return the create timestamp for the file''' # make sure we don't die if someone has #removed the file out from underneath us try: stats = os.stat(self.pathname()) except OSError: ctime = time.time() else: ctime = stats[stat.ST_CTIME] return ctime class MessageFactory(Corpus.MessageFactory): # Subclass must define a concrete message klass. klass = None def create(self, key, directory, content=None): '''Create a message object from a filename in a directory''' if content: msg = email.message_from_string(content, _class=self.klass) msg.file_name = key msg.directory = directory msg.loaded = True return msg return self.klass(key, directory) class FileMessageFactory(MessageFactory): '''MessageFactory for FileMessage objects''' klass = FileMessage class GzipFileMessage(FileMessage): '''Message that persists as a zipped file system artifact.''' def store(self): '''Write the Message substance to the file''' assert self.file_name is not None, \ "Must set filename before using FileMessage instances." if options["globals", "verbose"]: print 'storing', self.file_name pn = self.pathname() gz = gzip.open(pn, 'wb') gz.write(self.as_string()) gz.flush() gz.close() class GzipFileMessageFactory(MessageFactory): '''MessageFactory for FileMessage objects''' klass = GzipFileMessage spambayes-1.1a6/spambayes/hammie.py0000775000076500000240000002210611116605723017465 0ustar skipstaff00000000000000#! /usr/bin/env python import math from spambayes import mboxutils from spambayes import storage from spambayes.Options import options from spambayes.tokenizer import tokenize class Hammie: """A spambayes mail filter. This implements the basic functionality needed to score, filter, or train. """ def __init__(self, bayes, mode): self.bayes = bayes self.mode = mode def _scoremsg(self, msg, evidence=False): """Score a Message. msg can be a string, a file object, or a Message object. Returns the probability the message is spam. If evidence is true, returns a tuple: (probability, clues), where clues is a list of the words which contributed to the score. """ return self.bayes.spamprob(tokenize(msg), evidence) def formatclues(self, clues, sep="; "): """Format the clues into something readable.""" return sep.join(["%r: %.2f" % (word, prob) for word, prob in clues if (word[0] == '*' or prob <= options["Headers", "clue_mailheader_cutoff"] or prob >= 1.0 - options["Headers", "clue_mailheader_cutoff"])]) def score(self, msg, evidence=False): """Score (judge) a message. msg can be a string, a file object, or a Message object. Returns the probability the message is spam. If evidence is true, returns a tuple: (probability, clues), where clues is a list of the words which contributed to the score. """ return self._scoremsg(msg, evidence) def score_and_filter(self, msg, header=None, spam_cutoff=None, ham_cutoff=None, debugheader=None, debug=None, train=None): """Score (judge) a message and add a disposition header. msg can be a string, a file object, or a Message object. Optionally, set header to the name of the header to add, and/or spam_cutoff/ham_cutoff to the probability values which must be met or exceeded for a message to get a 'Spam' or 'Ham' classification. An extra debugging header can be added if 'debug' is set to True. The name of the debugging header is given as 'debugheader'. If 'train' is True, also train on the result of scoring the message (ie. train as ham if it's ham, train as spam if it's spam). If the message already has a trained header, it will be untrained first. You'll want to be very dilligent about retraining mistakes if you use this option. All defaults for optional parameters come from the Options file. Returns the score and same message with a new disposition header. """ if header == None: header = options["Headers", "classification_header_name"] if spam_cutoff == None: spam_cutoff = options["Categorization", "spam_cutoff"] if ham_cutoff == None: ham_cutoff = options["Categorization", "ham_cutoff"] if debugheader == None: debugheader = options["Headers", "evidence_header_name"] if debug == None: debug = options["Headers", "include_evidence"] if train == None: train = options["Hammie", "train_on_filter"] msg = mboxutils.get_message(msg) try: del msg[header] except KeyError: pass if train: self.untrain_from_header(msg) prob, clues = self._scoremsg(msg, True) if prob < ham_cutoff: is_spam = False disp = options["Headers", "header_ham_string"] elif prob > spam_cutoff: is_spam = True disp = options["Headers", "header_spam_string"] else: is_spam = False disp = options["Headers", "header_unsure_string"] if train: self.train(msg, is_spam, True) basic_disp = disp disp += "; %.*f" % (options["Headers", "header_score_digits"], prob) if options["Headers", "header_score_logarithm"]: if prob <= 0.005 and prob > 0.0: import math x = -math.log10(prob) disp += " (%d)" % x if prob >= 0.995 and prob < 1.0: x = -math.log10(1.0-prob) disp += " (%d)" % x del msg[header] msg.add_header(header, disp) # Obey notate_to and notate_subject. for header in ('to', 'subject'): if basic_disp in options["Headers", "notate_"+header]: orig = msg[header] del msg[header] msg[header] = "%s,%s" % (basic_disp, orig) if debug: disp = self.formatclues(clues) del msg[debugheader] msg.add_header(debugheader, disp) result = mboxutils.as_string(msg, unixfrom=(msg.get_unixfrom() is not None)) return prob, result def filter(self, msg, header=None, spam_cutoff=None, ham_cutoff=None, debugheader=None, debug=None, train=None): _prob, result = self.score_and_filter( msg, header, spam_cutoff, ham_cutoff, debugheader, debug, train) return result def train(self, msg, is_spam, add_header=False): """Train bayes with a message. msg can be a string, a file object, or a Message object. is_spam should be 1 if the message is spam, 0 if not. If add_header is True, add a header with how it was trained (in case we need to untrain later) """ self.bayes.learn(tokenize(msg), is_spam) if add_header: if is_spam: trained = options["Headers", "header_spam_string"] else: trained = options["Headers", "header_ham_string"] del msg[options["Headers", "trained_header_name"]] msg.add_header(options["Headers", "trained_header_name"], trained) def untrain(self, msg, is_spam): """Untrain bayes with a message. msg can be a string, a file object, or a Message object. is_spam should be True if the message is spam, False if not. """ self.bayes.unlearn(tokenize(msg), is_spam) def untrain_from_header(self, msg): """Untrain bayes based on X-Spambayes-Trained header. msg can be a string, a file object, or a Message object. If no such header is present, nothing happens. If add_header is True, add a header with how it was trained (in case we need to untrain later) """ msg = mboxutils.get_message(msg) trained = msg.get(options["Headers", "trained_header_name"]) if not trained: return del msg[options["Headers", "trained_header_name"]] if trained == options["Headers", "header_ham_string"]: self.untrain_ham(msg) elif trained == options["Headers", "header_spam_string"]: self.untrain_spam(msg) else: raise ValueError('%s header value unrecognized' % options["Headers", "trained_header_name"]) def train_ham(self, msg, add_header=False): """Train bayes with ham. msg can be a string, a file object, or a Message object. If add_header is True, add a header with how it was trained (in case we need to untrain later) """ self.train(msg, False, add_header) def train_spam(self, msg, add_header=False): """Train bayes with spam. msg can be a string, a file object, or a Message object. If add_header is True, add a header with how it was trained (in case we need to untrain later) """ self.train(msg, True, add_header) def untrain_ham(self, msg): """Untrain bayes with a message previously trained as ham. msg can be a string, a file object, or a Message object. """ self.untrain(msg, False) def untrain_spam(self, msg): """Untrain bayes with a message previously traned as spam. msg can be a string, a file object, or a Message object. """ self.untrain(msg, True) def store(self): """Write out the persistent store. This makes sure the persistent store reflects what is currently in memory. You would want to do this after a write and before exiting. """ self.bayes.store() def close(self): if self.mode != 'r': self.store() def open(filename, useDB="dbm", mode='r'): """Open a file, returning a Hammie instance. mode is used as the flag to open DBDict objects. 'c' for read-write (create if needed), 'r' for read-only, 'w' for read-write. """ return Hammie(storage.open_storage(filename, useDB, mode), mode) if __name__ == "__main__": # Everybody's used to running hammie.py. Why mess with success? ;) from spambayes import hammiebulk hammiebulk.main() spambayes-1.1a6/spambayes/hammiebulk.py0000775000076500000240000001450211116605734020346 0ustar skipstaff00000000000000#! /usr/bin/env python """Usage: %(program)s [-D|-d] [options] Where: -h show usage and exit -d FILE use the DBM store. A DBM file is larger than the pickle and creating it is slower, but loading it is much faster, especially for large word databases. Recommended for use with hammiefilter or any procmail-based filter. Default filename: %(DEFAULTDB)s -p FILE use the pickle store. A pickle is smaller and faster to create, but much slower to load. Recommended for use with sb_server and sb_xmlrpcserver. Default filename: %(DEFAULTDB)s -U Untrain instead of train. The interpretation of -g and -s remains the same. -f run as a filter: read a single message from stdin, add a new header, and write it to stdout. If you want to run from procmail, this is your option. -g PATH mbox or directory of known good messages (non-spam) to train on. Can be specified more than once, or use - for stdin. -s PATH mbox or directory of known spam messages to train on. Can be specified more than once, or use - for stdin. -u PATH mbox of unknown messages. A ham/spam decision is reported for each. Can be specified more than once. -r reverse the meaning of the check (report ham instead of spam). Only meaningful with the -u option. """ import sys import os import getopt from spambayes.Options import options, get_pathname_option from spambayes import mboxutils, hammie, Corpus, storage Corpus.Verbose = True program = sys.argv[0] # For usage(); referenced by docstring above # Default database name # This is a bit of a hack to counter the default for # persistent_storage_file changing from ~/.hammiedb to hammie.db # This will work unless a user had hammie.db as their value for # persistent_storage_file if options["Storage", "persistent_storage_file"] == \ options.default("Storage", "persistent_storage_file"): options["Storage", "persistent_storage_file"] = \ os.path.join("~", ".hammiedb") DEFAULTDB = get_pathname_option("Storage", "persistent_storage_file") # Probability at which a message is considered spam SPAM_THRESHOLD = options["Categorization", "spam_cutoff"] HAM_THRESHOLD = options["Categorization", "ham_cutoff"] def train(h, msgs, is_spam): """Train bayes with all messages from a mailbox.""" mbox = mboxutils.getmbox(msgs) i = 0 for msg in mbox: i += 1 if i % 10 == 0: sys.stdout.write("\r%6d" % i) sys.stdout.flush() h.train(msg, is_spam) sys.stdout.write("\r%6d" % i) sys.stdout.flush() print def untrain(h, msgs, is_spam): """Untrain bayes with all messages from a mailbox.""" mbox = mboxutils.getmbox(msgs) i = 0 for msg in mbox: i += 1 if i % 10 == 0: sys.stdout.write("\r%6d" % i) sys.stdout.flush() h.untrain(msg, is_spam) sys.stdout.write("\r%6d" % i) sys.stdout.flush() print def score(h, msgs, reverse=0): """Score (judge) all messages from a mailbox.""" # XXX The reporting needs work! mbox = mboxutils.getmbox(msgs) i = 0 spams = hams = unsures = 0 for msg in mbox: i += 1 prob, clues = h.score(msg, True) if hasattr(msg, '_mh_msgno'): msgno = msg._mh_msgno else: msgno = i isspam = (prob >= SPAM_THRESHOLD) isham = (prob <= HAM_THRESHOLD) if isspam: spams += 1 if not reverse: print "%6s %4.2f %1s" % (msgno, prob, isspam and "S" or "."), print h.formatclues(clues) elif isham: hams += 1 if reverse: print "%6s %4.2f %1s" % (msgno, prob, isham and "S" or "."), print h.formatclues(clues) else: unsures += 1 print "%6s %4.2f U" % (msgno, prob), print h.formatclues(clues) return (spams, hams, unsures) def usage(code, msg=''): """Print usage message and sys.exit(code).""" if msg: print >> sys.stderr, msg print >> sys.stderr print >> sys.stderr, __doc__ % globals() sys.exit(code) def main(): """Main program; parse options and go.""" try: opts, args = getopt.getopt(sys.argv[1:], 'hd:Ufg:s:p:u:r') except getopt.error, msg: usage(2, msg) if not opts: usage(2, "No options given") pck = DEFAULTDB good = [] spam = [] unknown = [] reverse = 0 untrain_mode = 0 do_filter = False usedb = None mode = 'r' for opt, arg in opts: if opt == '-h': usage(0) elif opt == '-g': good.append(arg) mode = 'c' elif opt == '-s': spam.append(arg) mode = 'c' elif opt == "-f": do_filter = True elif opt == '-u': unknown.append(arg) elif opt == '-U': untrain_mode = 1 elif opt == '-r': reverse = 1 pck, usedb = storage.database_type(opts) if args: usage(2, "Positional arguments not allowed") if usedb == None: usage(2, "Must specify one of -d or -D") save = False h = hammie.open(pck, usedb, mode) if not untrain_mode: for g in good: print "Training ham (%s):" % g train(h, g, False) save = True for s in spam: print "Training spam (%s):" % s train(h, s, True) save = True else: for g in good: print "Untraining ham (%s):" % g untrain(h, g, False) save = True for s in spam: print "Untraining spam (%s):" % s untrain(h, s, True) save = True if save: h.store() if do_filter: msg = sys.stdin.read() filtered = h.filter(msg) sys.stdout.write(filtered) if unknown: spams = hams = unsures = 0 for u in unknown: if len(unknown) > 1: print "Scoring", u s, g, u = score(h, u, reverse) spams += s hams += g unsures += u print "Total %d spam, %d ham, %d unsure" % (spams, hams, unsures) if __name__ == "__main__": main() spambayes-1.1a6/spambayes/Histogram.py0000664000076500000240000001422311116605740020157 0ustar skipstaff00000000000000#! /usr/bin/env python import math from spambayes.Options import options class Hist: """Simple histograms of float values.""" # Pass None for lo and hi and it will automatically adjust to the min # and max values seen. # Note: nbuckets can be passed for backward compatibility. The # display() method can be passed a different nbuckets value. def __init__(self, nbuckets=options["TestDriver", "nbuckets"], lo=0.0, hi=100.0): self.lo, self.hi = lo, hi self.nbuckets = nbuckets self.buckets = [0] * nbuckets self.data = [] # the raw data points self.stats_uptodate = False # Add a value to the collection. def add(self, x): self.data.append(x) self.stats_uptodate = False # Compute, and set as instance attrs: # n # of data points # The rest are set iff n>0: # min smallest value in collection # max largest value in collection # median midpoint # mean # pct list of (percentile, score) pairs # var variance # sdev population standard deviation (sqrt(variance)) # self.data is also sorted. def compute_stats(self): if self.stats_uptodate: return self.stats_uptodate = True data = self.data n = self.n = len(data) if n == 0: return data.sort() self.min = data[0] self.max = data[-1] if n & 1: self.median = data[n // 2] else: self.median = (data[n // 2] + data[(n-1) // 2]) / 2.0 # Compute mean. # Add in increasing order of magnitude, to minimize roundoff error. if data[0] < 0.0: temp = [(abs(x), x) for x in data] temp.sort() data = [x[1] for x in temp] del temp sum = 0.0 for x in data: sum += x mean = self.mean = sum / n # Compute variance. var = 0.0 for x in data: d = x - mean var += d*d self.var = var / n self.sdev = math.sqrt(self.var) # Compute percentiles. self.pct = pct = [] for p in options["TestDriver", "percentiles"]: assert 0.0 <= p <= 100.0 # In going from data index 0 to index n-1, we move n-1 times. # p% of that is (n-1)*p/100. i = (n-1)*p/1e2 if i < 0: # Just return the smallest. score = data[0] else: whole = int(i) frac = i - whole score = data[whole] if whole < n-1 and frac: # Move frac of the way from this score to the next. score += frac * (data[whole + 1] - score) pct.append((p, score)) # Merge other into self. def __iadd__(self, other): self.data.extend(other.data) self.stats_uptodate = False return self def get_lo_hi(self): self.compute_stats() lo, hi = self.lo, self.hi if lo is None: lo = self.min if hi is None: hi = self.max return lo, hi def get_bucketwidth(self): lo, hi = self.get_lo_hi() span = float(hi - lo) return span / self.nbuckets # Set instance var nbuckets to the # of buckets, and buckets to a list # of nbuckets counts. def fill_buckets(self, nbuckets=None): if nbuckets is None: nbuckets = self.nbuckets if nbuckets <= 0: raise ValueError("nbuckets %g > 0 required" % nbuckets) self.nbuckets = nbuckets self.buckets = buckets = [0] * nbuckets # Compute bucket counts. lo, hi = self.get_lo_hi() bucketwidth = self.get_bucketwidth() for x in self.data: i = int((x - lo) / bucketwidth) if i >= nbuckets: i = nbuckets - 1 elif i < 0: i = 0 buckets[i] += 1 # Print a histogram to stdout. # Also sets instance var nbuckets to the # of buckets, and # buckts to a list of nbuckets counts, but only if at least one # data point is in the collection. def display(self, nbuckets=None, WIDTH=61): if nbuckets is None: nbuckets = self.nbuckets if nbuckets <= 0: raise ValueError("nbuckets %g > 0 required" % nbuckets) self.compute_stats() n = self.n if n == 0: return print "%d items; mean %.2f; sdev %.2f" % (n, self.mean, self.sdev) print "-> min %g; median %g; max %g" % (self.min, self.median, self.max) pcts = ['%g%% %g' % x for x in self.pct] print "-> percentiles:", '; '.join(pcts) lo, hi = self.get_lo_hi() if lo > hi: return # hunit is how many items a * represents. A * is printed for # each hunit items, plus any non-zero fraction thereof. self.fill_buckets(nbuckets) biggest = max(self.buckets) hunit, r = divmod(biggest, WIDTH) if r: hunit += 1 print "* =", hunit, "items" # We need ndigits decimal digits to display the largest bucket count. ndigits = len(str(biggest)) # Displaying the bucket boundaries is more troublesome. bucketwidth = self.get_bucketwidth() whole_digits = max(len(str(int(lo))), len(str(int(hi - bucketwidth)))) frac_digits = 0 while bucketwidth < 1.0: # Incrementing by bucketwidth may not change the last displayed # digit, so display one more. frac_digits += 1 bucketwidth *= 10.0 format = ("%" + str(whole_digits + 1 + frac_digits) + '.' + str(frac_digits) + 'f %' + str(ndigits) + "d") bucketwidth = self.get_bucketwidth() for i in range(nbuckets): n = self.buckets[i] print format % (lo + i * bucketwidth, n), print '*' * ((n + hunit - 1) // hunit) spambayes-1.1a6/spambayes/i18n.py0000775000076500000240000001741411116610637017012 0ustar skipstaff00000000000000"""Internationalisation Classes: LanguageManager - Interface class for languages. Abstract: Manages the internationalisation (i18n) aspects of SpamBayes. """ # This module is part of the spambayes project, which is Copyright 2002-2007 # The Python Software Foundation and is covered by the Python Software # Foundation license. __author__ = "Hernan Martinez Foffani " __credits__ = "Tony Meyer, All the SpamBayes folk." import os import sys from locale import getdefaultlocale from gettext import translation, NullTranslations # Note, we must not import spambayes.Options, or Outlook will not be happy. ## Set language environment for gettext and for dynamic load of dialogs. ## ## Our directory layout with source is: ## spambayes ## spambayes (or spambayes.modules with the binary) ## i18n.py <-- this file ## languages <-- the directory for lang packs ## es <-- generic language data ## DIALOGS ## LC_MESSAGES <-- gettext message files ## __init__.py <-- resourcepackage __init__.py ## ui.html <-- web interface translation ## es_ES <-- specific language/country data. ## DIALOGS <-- resource dialogs ## LC_MESSAGES <-- gettext message files ## __init__.py <-- resourcepackage __init__.py ## ui.html <-- web interface translation ## zn ## zn_TW ## Outlook2000 ## utilities ## ..etc.. ## ## Our directory layout with the binaries is: ## lib ## spambayes.modules ## i18n.py <-- this file ## languages <-- the directory for lang packs ## es <-- generic language data ## DIALOGS ## __init__.py <-- resourcepackage __init__.py ## ui.html <-- web interface translation ## es_ES <-- specific language/country data. ## DIALOGS <-- resource dialogs ## __init__.py <-- resourcepackage __init__.py ## ui.html <-- web interface translation ## zn ## zn_TW ## languages ## es <-- generic language data ## LC_MESSAGES <-- gettext message files ## es_ES <-- specific language/country data. ## LC_MESSAGES <-- gettext message files ## zn ## zn_TW ## ..etc.. ## ## A distutils installation will not currently work. I don't know ## where to put the .mo files, so setup.py ignores them. if hasattr(sys, "frozen"): # LC_MESSAGES are harder to find. if sys.frozen == "dll": # Outlook import win32api this_filename = win32api.GetModuleFileName(sys.frozendllhandle) else: # Not Outlook this_filename = __file__ LC_DIR = os.path.dirname(os.path.dirname(this_filename)) LANG_DIR = os.path.join(os.path.dirname(__file__), "languages") else: this_filename = os.path.abspath(__file__) LANG_DIR = os.path.join(os.path.dirname(this_filename), "languages") LC_DIR = LANG_DIR class LanguageManager: def __init__(self): self.current_langs_codes = [] self._sys_path_modifications = [] def set_language(self, lang_code=None): """Set a language as the current one.""" if not lang_code: return self.current_langs_codes = [ lang_code ] self._rebuild_syspath_for_dialogs() self._install_gettext() def locale_default_lang(self): """Get the default language for the locale.""" # Note that this may return None. try: return os.environ["SPAMBAYES_LANG"] except KeyError: return getdefaultlocale()[0] def add_language(self, lang_code=None): """Add a language to the current languages list. The list acts as a fallback mechanism, where the first language of the list is used if possible, and if not the second one, and so on. """ if not lang_code: return self.current_langs_codes.insert(0, lang_code) self._rebuild_syspath_for_dialogs() self._install_gettext() def clear_language(self): """Clear the current language(s) and set SpamBayes to use the default.""" self.current_langs_codes = [] self._clear_syspath() lang = NullTranslations() lang.install() def import_ui_html(self): """Load and return the appropriate ui_html.py module for the current language.""" for language in self.current_langs_codes: moduleName = 'spambayes.languages.%s.i18n_ui_html' % (language, ) try: module = __import__(moduleName, {}, {}, ('spambayes.languages', language)) except ImportError: # That language isn't available - fall back to the # next one. pass else: return module # Nothing available - use the default. from spambayes.resources import ui_html return ui_html def _install_gettext(self): """Set the gettext specific environment.""" lang = translation("messages", LC_DIR, self.current_langs_codes, fallback=True) lang.install() def _rebuild_syspath_for_dialogs(self): """Add to sys.path the directories of the translated dialogs. For each language of the current list, we add two directories, one for language code and country and the other for the language code only, so we can simulate the fallback procedures.""" self._clear_syspath() for lcode in self.current_langs_codes: code_and_country = os.path.join(LANG_DIR, lcode, 'DIALOGS') code_only = os.path.join(LANG_DIR, lcode.split("_")[0], 'DIALOGS') if code_and_country not in sys.path: sys.path.append(code_and_country) self._sys_path_modifications.append(code_and_country) if code_only not in sys.path: sys.path.append(code_only) self._sys_path_modifications.append(code_only) def _clear_syspath(self): """Clean sys.path of the stuff that we put in it.""" for path in self._sys_path_modifications: sys.path.remove(path) self._sys_path_modifications = [] def test(): lm = LanguageManager() print "INIT: len(sys.path): ", len(sys.path) print "TEST default lang" lm.set_language(lm.locale_default_lang()) print "\tCurrent Languages: ", lm.current_langs_codes print "\tlen(sys.path): ", len(sys.path) print "\t", _("Help") print "TEST clear_language" lm.clear_language() print "\tCurrent Languages: ", lm.current_langs_codes print "\tlen(sys.path): ", len(sys.path) print "\t", _("Help") print "TEST set_language" for langcode in ["kk_KK", "z", "", "es", None, "es_AR"]: print "lang: ", langcode lm.set_language(langcode) print "\tCurrent Languages: ", lm.current_langs_codes print "\tlen(sys.path): ", len(sys.path) print "\t", _("Help") lm.clear_language() print "TEST add_language" for langcode in ["kk_KK", "z", "", "es", None, "es_AR"]: print "lang: ", langcode lm.add_language(langcode) print "\tCurrent Languages: ", lm.current_langs_codes print "\tlen(sys.path): ", len(sys.path) print "\t", _("Help") if __name__ == '__main__': test() spambayes-1.1a6/spambayes/ImageStripper.py0000664000076500000240000003346111116632073021001 0ustar skipstaff00000000000000""" This is the place where we try and discover information buried in images. """ from __future__ import division import sys import os import tempfile import math import atexit try: import cStringIO as StringIO except ImportError: import StringIO try: from PIL import Image, ImageSequence except ImportError: Image = None from spambayes.safepickle import pickle_read, pickle_write from spambayes.port import md5 # The email mime object carrying the image data can have a special attribute # which indicates that a message had an image, but it was large (ie, larger # than the 'max_image_size' option.) This allows the provider of the email # object to avoid loading huge images into memory just to have this image # stripper ignore it. # If the attribute exists, it should be the size of the image (we assert it # is > max_image_size). The image payload is ignored. # A 'cleaner' option would be to look at a header - but an attribute was # chosen to avoid spammers getting wise and 'injecting' the header into the # message body of a mime section. image_large_size_attribute = "spambayes_image_large_size" from spambayes.Options import options # copied from tokenizer.py - maybe we should split it into pieces... def log2(n, log=math.log, c=math.log(2)): return log(n)/c def is_executable(prog): if sys.platform == "win32": return True info = os.stat(prog) return (info.st_uid == os.getuid() and (info.st_mode & 0100) or info.st_gid == os.getgid() and (info.st_mode & 0010) or info.st_mode & 0001) def find_program(prog): path = os.environ.get("PATH", "").split(os.pathsep) if sys.platform == "win32": prog = "%s.exe" % prog if hasattr(sys, "frozen"): # a binary (py2exe) build.. # Outlook plugin puts executables in (for example): # C:/Program Files/SpamBayes/bin # so add that directory to the path and make sure we # look for a file ending in ".exe". if sys.frozen == "dll": import win32api sentinal = win32api.GetModuleFileName(sys.frozendllhandle) else: sentinal = sys.executable # os.popen() trying to quote both the program and argv[1] fails. # So just use the short version. # For the sake of safety, in a binary build we *only* look in # our bin dir. path = [win32api.GetShortPathName(os.path.dirname(sentinal))] else: # a source build - for testing, allow it in SB package dir. import spambayes path.insert(0, os.path.abspath(spambayes.__path__[0])) for directory in path: program = os.path.join(directory, prog) if os.path.exists(program) and is_executable(program): return program return "" def imconcatlr(left, right): """Concatenate two images left to right.""" w1, h1 = left.size w2, h2 = right.size result = Image.new("RGB", (w1 + w2, max(h1, h2))) result.paste(left, (0, 0)) result.paste(right, (w1, 0)) return result def imconcattb(upper, lower): """Concatenate two images top to bottom.""" w1, h1 = upper.size w2, h2 = lower.size result = Image.new("RGB", (max(w1, w2), h1 + h2)) result.paste(upper, (0, 0)) result.paste(lower, (0, h1)) return result def PIL_decode_parts(parts): """Decode and assemble a bunch of images using PIL.""" tokens = set() rows = [] max_image_size = options["Tokenizer", "max_image_size"] for part in parts: # See 'image_large_size_attribute' above - the provider may have seen # an image, but optimized the fact we don't bother processing large # images. nbytes = getattr(part, image_large_size_attribute, None) if nbytes is None: # no optimization - process normally... try: bytes = part.get_payload(decode=True) nbytes = len(bytes) except: tokens.add("invalid-image:%s" % part.get_content_type()) continue else: # optimization should not have remove images smaller than our max assert nbytes > max_image_size, (len(bytes), max_image_size) if nbytes > max_image_size: tokens.add("image:big") continue # assume it's just a picture for now # We're dealing with spammers and virus writers here. Who knows # what garbage they will call a GIF image to entice you to open # it? try: image = Image.open(StringIO.StringIO(bytes)) image.load() except: # Any error whatsoever is reason for not looking further at # the image. tokens.add("invalid-image:%s" % part.get_content_type()) continue else: # Spammers are now using GIF image sequences. From examining a # miniscule set of multi-frame GIFs it appears the frame with # the fewest number of background pixels is the one with the # text content. if "duration" in image.info: # Big assumption? I don't know. If the image's info dict # has a duration key assume it's a multi-frame image. This # should save some needless construction of pixel # histograms for single-frame images. bgpix = 1e17 # ridiculously large number of pixels try: for frame in ImageSequence.Iterator(image): # Assume the pixel with the largest value is the # background. bg = max(frame.histogram()) if bg < bgpix: image = frame bgpix = bg # I've empirically determined: # * ValueError => GIF image isn't multi-frame. # * IOError => Decoding error except IOError: tokens.add("invalid-image:%s" % part.get_content_type()) continue except ValueError: pass image = image.convert("RGB") if not rows: # first image rows.append(image) elif image.size[1] != rows[-1].size[1]: # new image, different height => start new row rows.append(image) else: # new image, same height => extend current row rows[-1] = imconcatlr(rows[-1], image) if not rows: return [], tokens # now concatenate the resulting row images top-to-bottom full_image, rows = rows[0], rows[1:] for image in rows: full_image = imconcattb(full_image, image) fd, pnmfile = tempfile.mkstemp('-spambayes-image') os.close(fd) full_image.save(open(pnmfile, "wb"), "PPM") return [pnmfile], tokens class OCREngine(object): """Base class for an OCR "engine" that extracts text. Ideally would also deal with image format (as different engines will have different requirements), but all currently supported ones deal with the PNM formats (ppm/pgm/pbm) """ engine_name = None # sub-classes should override. def __init__(self): pass def is_enabled(self): """Return true if this engine is able to be used. Note that returning true only means it is *capable* of being used - not that it is enabled. eg, it should check the program is needs to use is installed, etc. """ raise NotImplementedError def extract_text(self, pnmfiles): """Extract the text as an unprocessed stream (but as a string). Typically this will be the raw output from the OCR engine. """ raise NotImplementedError class OCRExecutableEngine(OCREngine): """Uses a simple executable that writes to stdout to extract the text""" engine_name = None def __init__(self): # we go looking for the program first use and cache its location self._program = None OCREngine.__init__(self) def is_enabled(self): return self.program is not None def get_program(self): # by default, executable is same as engine name if not self._program: self._program = find_program(self.engine_name) return self._program program = property(get_program) def get_command_line(self, pnmfile): raise NotImplementedError, "base classes must override" def extract_text(self, pnmfile): # Generically reads output from stdout. assert self.is_enabled(), "I'm not working!" cmdline = self.get_command_line(pnmfile) ocr = os.popen(cmdline) ret = ocr.read() exit_code = ocr.close() if exit_code: raise SystemError, ("%s failed with exit code %s" % (self.engine_name, exit_code)) return ret class OCREngineOCRAD(OCRExecutableEngine): engine_name = "ocrad" def get_command_line(self, pnmfile): scale = options["Tokenizer", "ocrad_scale"] or 1 charset = options["Tokenizer", "ocrad_charset"] return '%s -s %s -c %s -f "%s" 2>%s' % \ (self.program, scale, charset, pnmfile, os.path.devnull) class OCREngineGOCR(OCRExecutableEngine): engine_name = "gocr" def get_command_line(self, pnmfile): return '%s "%s" 2>%s' % (self.program, pnmfile, os.path.devnull) # This lists all engines, with the first listed that is enabled winning. # Matched with the engine name, as specified in Options.py, via the # 'engine_name' attribute on the class. _ocr_engines = [ OCREngineGOCR, OCREngineOCRAD, ] def get_engine(engine_name): if not engine_name: candidates = _ocr_engines else: for e in _ocr_engines: if e.engine_name == engine_name: candidates = [e] break else: candidates = [] for candidate in candidates: engine = candidate() if engine.is_enabled(): return engine return None class ImageStripper: def __init__(self, cachefile=""): self.cachefile = os.path.expanduser(cachefile) if os.path.exists(self.cachefile): self.cache = pickle_read(self.cachefile) else: self.cache = {} self.misses = self.hits = 0 if self.cachefile: atexit.register(self.close) self.engine = None def extract_ocr_info(self, pnmfiles): assert self.engine, "must have an engine!" textbits = [] tokens = set() for pnmfile in pnmfiles: preserve = False fhash = md5(open(pnmfile).read()).hexdigest() if fhash in self.cache: self.hits += 1 ctext, ctokens = self.cache[fhash] else: self.misses += 1 if self.engine.program: try: ctext = self.engine.extract_text(pnmfile).lower() except SystemError, msg: print >> sys.stderr, msg preserve = True ctext = "" else: # We should not get here if no OCR is enabled. If it # is enabled and we have no program, its OK to spew lots # of warnings - they should either disable OCR (it is by # default), or fix their config. print >> sys.stderr, \ "No OCR program '%s' available - can't get text!" \ % (self.engine.engine_name,) ctext = "" ctokens = set() if not ctext.strip(): # Lots of spam now contains images in which it is # difficult or impossible (using ocrad) to find any # text. Make a note of that. ctokens.add("image-text:no text found") else: nlines = len(ctext.strip().split("\n")) if nlines: ctokens.add("image-text-lines:%d" % int(log2(nlines))) self.cache[fhash] = (ctext, ctokens) textbits.append(ctext) tokens |= ctokens if not preserve: os.unlink(pnmfile) return "\n".join(textbits), tokens def analyze(self, engine_name, parts): # check engine hasn't changed... if self.engine is not None and self.engine.engine_name != engine_name: self.engine = None # check engine exists and is valid if self.engine is None: self.engine = get_engine(engine_name) if self.engine is None: # We only get here if explicitly enabled - spewing msgs is ok. print >> sys.stderr, "invalid engine name '%s' - OCR disabled" \ % (engine_name,) return "", set() if not parts: return "", set() if Image is not None: pnmfiles, tokens = PIL_decode_parts(parts) else: return "", set() if pnmfiles: text, new_tokens = self.extract_ocr_info(pnmfiles) return text, tokens | new_tokens return "", tokens def close(self): if options["globals", "verbose"]: print >> sys.stderr, "saving", len(self.cache), print >> sys.stderr, "items to", self.cachefile, if self.hits + self.misses: print >> sys.stderr, "%.2f%% hit rate" % \ (100 * self.hits / (self.hits + self.misses)), print >> sys.stderr pickle_write(self.cachefile, self.cache) _cachefile = options["Tokenizer", "crack_image_cache"] crack_images = ImageStripper(_cachefile).analyze spambayes-1.1a6/spambayes/ImapUI.py0000664000076500000240000003417411116605750017356 0ustar skipstaff00000000000000"""IMAPFilter Web Interface Classes: IMAPUserInterface - Interface class for the IMAP filter Abstract: This module implements a browser based Spambayes user interface for the IMAP filter. Users may use it to interface with the filter - it is expected that this will primarily be for configuration, although users may also wish to look up words in the database, or classify a message. The following functions are currently included: [From the base class UserInterface] onClassify - classify a given message onWordquery - query a word from the database onTrain - train a message or mbox onSave - save the database and possibly shutdown [Here] onHome - a home page with various options To do: o This could have a neat review page, like pop3proxy, built up by asking the IMAP server appropriate questions. I don't know whether this is needed, however. This would then allow viewing a message, showing the clues for it, and so on. Finding a message (via the spambayes id) could also be done. o Suggestions? """ # This module is part of the spambayes project, which is Copyright 2002-2007 # The Python Software Foundation and is covered by the Python Software # Foundation license. __author__ = "Tony Meyer , Tim Stone" __credits__ = "All the Spambayes folk." import cgi from spambayes import UserInterface from spambayes.Options import options, optionsPathname, _ # These are the options that will be offered on the configuration page. # If the option is None, then the entry is a header and the following # options will appear in a new box on the configuration page. # These are also used to generate http request parameters and template # fields/variables. parm_map = ( (_('IMAP Options'), None), ('imap', 'server'), ('imap', 'username'), # to display, or not to display; that is the question! # if we show this here, it's in plain text for everyone to # see (and worse - if we don't restrict connections to # localhost, it's available for the world to see) # on the other hand, we have to be able to enter it somehow... ('imap', 'password'), ('imap', 'use_ssl'), (_('Header Options'), None), ('Headers', 'notate_to'), ('Headers', 'notate_subject'), (_('Storage Options'), None), ('Storage', 'persistent_storage_file'), ('Storage', 'messageinfo_storage_file'), (_('Statistics Options'), None), ('Categorization', 'ham_cutoff'), ('Categorization', 'spam_cutoff'), ) # Like the above, but hese are the options that will be offered on the # advanced configuration page. adv_map = ( (_('Statistics Options'), None), ('Classifier', 'max_discriminators'), ('Classifier', 'minimum_prob_strength'), ('Classifier', 'unknown_word_prob'), ('Classifier', 'unknown_word_strength'), ('Classifier', 'use_bigrams'), (_('Header Options'), None), ('Headers', 'include_score'), ('Headers', 'header_score_digits'), ('Headers', 'header_score_logarithm'), ('Headers', 'include_thermostat'), ('Headers', 'include_evidence'), ('Headers', 'clue_mailheader_cutoff'), (_('Storage Options'), None), ('Storage', 'persistent_use_database'), (_('Tokenising Options'), None), ('Tokenizer', 'mine_received_headers'), ('Tokenizer', 'replace_nonascii_chars'), ('Tokenizer', 'summarize_email_prefixes'), ('Tokenizer', 'summarize_email_suffixes'), ('Tokenizer', 'x-pick_apart_urls'), (_('Interface Options'), None), ('html_ui', 'display_adv_find'), ('html_ui', 'allow_remote_connections'), ('html_ui', 'http_authentication'), ('html_ui', 'http_user_name'), ('html_ui', 'http_password'), ('globals', 'language'), ) # This is here because we need to refer to it here, and in sb_imapfilter. # I suppose it really belongs somewhere else, where both can refer to it, # but there isn't any such place, and creating it just for this is rather # pointless. class LoginFailure(Exception): """Login to the IMAP server failed.""" def __init__(self, details): self.details = details def __str__(self): return "Login failure: %s" % (self.details,) class IMAPUserInterface(UserInterface.UserInterface): """Serves the HTML user interface for the proxies.""" def __init__(self, cls, imaps, pwds, imap_session_class, lang_manager=None, stats=None, close_db=None, change_db=None): global parm_map # Only offer SSL if it is available try: from imaplib import IMAP4_SSL except ImportError: parm_list = list(parm_map) parm_list.remove(("imap", "use_ssl")) parm_map = tuple(parm_list) else: del IMAP4_SSL UserInterface.UserInterface.__init__(self, cls, parm_map, adv_map, lang_manager, stats) self.classifier = cls self.imaps = imaps self.imap_pwds = pwds self.app_for_version = "SpamBayes IMAP Filter" self.imap_session_class = imap_session_class self.close_database = close_db self.change_db = change_db def onHome(self): """Serve up the homepage.""" stateDict = self.classifier.__dict__.copy() stateDict["warning"] = "" stateDict.update(self.classifier.__dict__) statusTable = self.html.statusTable.clone() del statusTable.proxyDetails # This could be a bit more modular statusTable.configurationLink += "
        " \ " " + _("You can also configure" \ " folders to filter
    and " \ "Configure folders to" \ " train") findBox = self._buildBox(_('Word query'), 'query.gif', self.html.wordQuery) if not options["html_ui", "display_adv_find"]: del findBox.advanced content = (self._buildBox(_('Status and Configuration'), 'status.gif', statusTable % stateDict)+ self._buildTrainBox() + self._buildClassifyBox() + findBox ) self._writePreamble(_("Home")) self.write(content) self._writePostamble() def reReadOptions(self): """Called by the config page when the user saves some new options, or restores the defaults.""" # Re-read the options. import Options Options.load_options() global options from Options import options self.change_db() def onSave(self, how): for imap in self.imaps: if imap: imap.logout() UserInterface.UserInterface.onSave(self, how) def onFilterfolders(self): self._writePreamble(_("Select Filter Folders")) self._login_to_imap() available_folders = [] for imap in self.imaps: if imap and imap.logged_in: available_folders.extend(imap.folder_list()) if not available_folders: content = self._buildBox(_("Error"), None, _("No folders available")) self.write(content) self._writePostamble() return content = self.html.configForm.clone() content.configFormContent = "" content.introduction = _("This page allows you to change " \ "which folders are filtered, and " \ "where filtered mail ends up.") content.config_submit.value = _("Save Filter Folders") content.optionsPathname = optionsPathname for opt in ("unsure_folder", "spam_folder", "filter_folders"): folderBox = self._buildFolderBox("imap", opt, available_folders) content.configFormContent += folderBox self.write(content) self._writePostamble() def _login_to_imap(self): new_imaps = [] for i in xrange(len(self.imaps)): imap = self.imaps[i] imap_logged_in = self._login_to_imap_server(imap, i) if imap_logged_in: new_imaps.append(imap_logged_in) self.imaps = new_imaps def _login_to_imap_server(self, imap, i): if imap and imap.logged_in: return imap if imap is None or not imap.connected: try: server = options["imap", "server"][i] except KeyError: content = self._buildBox(_("Error"), None, _("Please check server/port details.")) self.write(content) return None if server.find(':') > -1: server, port = server.split(':', 1) port = int(port) else: if options["imap", "use_ssl"]: port = 993 else: port = 143 imap = self.imap_session_class(server, port) if not imap.connected: # Failed to connect. content = self._buildBox(_("Error"), None, _("Please check server/port details.")) self.write(content) return None usernames = options["imap", "username"] if not usernames: content = self._buildBox(_("Error"), None, _("Must specify username first.")) self.write(content) return None if not self.imap_pwds: self.imap_pwd = options["imap", "password"] if not self.imap_pwds: content = self._buildBox(_("Error"), None, _("Must specify password first.")) self.write(content) return None try: imap.login(usernames[i], self.imap_pwds[i]) except KeyError: content = self._buildBox(_("Error"), None, _("Please check username/password details.")) self.write(content) return None except LoginFailure, e: content = self._buildBox(_("Error"), None, str(e)) self.write(content) return None return imap def onTrainingfolders(self): self._writePreamble(_("Select Training Folders")) self._login_to_imap() available_folders = [] for imap in self.imaps: if imap and imap.logged_in: available_folders.extend(imap.folder_list()) if not available_folders: content = self._buildBox(_("Error"), None, _("No folders available")) self.write(content) self._writePostamble() return content = self.html.configForm.clone() content.configFormContent = "" content.introduction = _("This page allows you to change " \ "which folders contain mail to " \ "train Spambayes.") content.config_submit.value = _("Save Training Folders") content.optionsPathname = optionsPathname for opt in ("ham_train_folders", "spam_train_folders"): folderBox = self._buildFolderBox("imap", opt, available_folders) content.configFormContent += folderBox self.write(content) self._writePostamble() def onChangeopts(self, **parms): backup = self.parm_ini_map if parms["how"] == _("Save Training Folders") or \ parms["how"] == _("Save Filter Folders"): del parms["how"] self.parm_ini_map = () for opt, value in parms.items(): del parms[opt] # Under strange circumstances this could break, # so if we can think of a better way to do this, # that would be nice. if opt[-len(value):] == value: opt = opt[:-len(value)] self.parm_ini_map += ("imap", opt), key = "imap_" + opt if parms.has_key(key): parms[key] += ',' + value else: parms[key] = value UserInterface.UserInterface.onChangeopts(self, **parms) self.parm_ini_map = backup def _buildFolderBox(self, section, option, available_folders): folderTable = self.html.configTable.clone() del folderTable.configTextRow1 del folderTable.configTextRow2 del folderTable.configCbRow1 del folderTable.configRow2 del folderTable.blankRow del folderTable.folderRow firstRow = True for folder in available_folders: folder = cgi.escape(folder) folderRow = self.html.configTable.folderRow.clone() if firstRow: folderRow.helpCell = options.doc(section, option) firstRow = False else: del folderRow.helpCell folderRow.folderBox.name = option folderRow.folderBox.value = folder folderRow.folderName = folder if options.multiple_values_allowed(section, option): if folder in options[section, option]: folderRow.folderBox.checked = "checked" folderRow.folderBox.name += folder else: if folder == options[section, option]: folderRow.folderBox.checked = "checked" folderRow.folderBox.type = "radio" folderTable += folderRow return self._buildBox(options.display_name(section, option), None, folderTable) spambayes-1.1a6/spambayes/languages/0000775000076500000240000000000011355064627017624 5ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/__init__.py0000775000076500000240000000000010646440127021721 0ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/de/0000775000076500000240000000000011355064627020214 5ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/de/__init__.py0000664000076500000240000000213211116610705022310 0ustar skipstaff00000000000000"""Design-time __init__.py for resourcepackage This is the scanning version of __init__.py for your resource modules. You replace it with a blank or doc-only init when ready to release. """ import os if os.path.splitext(os.path.basename( __file__ ))[0] == "__init__": try: from resourcepackage import package, defaultgenerators generators = defaultgenerators.generators.copy() ### CUSTOMISATION POINT ## import specialised generators here, such as for wxPython #from resourcepackage import wxgenerators #generators.update( wxgenerators.generators ) except ImportError: pass else: package = package.Package( packageName = __name__, directory = os.path.dirname( os.path.abspath(__file__) ), generators = generators, ) package.scan( ### CUSTOMISATION POINT ## force true -> always re-loads from external files, otherwise ## only reloads if the file is newer than the generated .py file. # force = 1, ) spambayes-1.1a6/spambayes/languages/de/_cvsignore.py0000664000076500000240000000032311147407305022713 0ustar skipstaff00000000000000# -*- coding: ISO-8859-1 -*- """Resource _cvsignore (from file .cvsignore)""" # written by resourcepackage: (1, 0, 0) source = '.cvsignore' package = 'spambayes.languages.de' data = "*.pyc\012*.pyo\012" ### end spambayes-1.1a6/spambayes/languages/de/DIALOGS/0000775000076500000240000000000011355064627021276 5ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/de/DIALOGS/__init__.py0000664000076500000240000000000010646440126023367 0ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/de/DIALOGS/dialogs.rc0000664000076500000240000007277610646440126023263 0ustar skipstaff00000000000000//Microsoft Developer Studio generated resource script. // #include "dialogs.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" // spambayes dialog definitions ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // Deutsch (Deutschland) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_DEU) #ifdef _WIN32 LANGUAGE LANG_GERMAN, SUBLANG_GERMAN #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Dialog // IDD_ADVANCED DIALOGEX 0, 0, 248, 257 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP CAPTION "Erweitert" FONT 8, "Tahoma" BEGIN GROUPBOX "Zeitliches Verhalten",IDC_STATIC,7,3,234,117 CONTROL "",IDC_DELAY1_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,16,36,148,22 LTEXT "Wartezeit vor dem start",IDC_STATIC,16,26,101,8 EDITTEXT IDC_DELAY1_TEXT,165,39,40,14,ES_AUTOHSCROLL LTEXT "seconds",IDC_STATIC,208,41,28,8 CONTROL "",IDC_DELAY2_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,16,73,148,22 LTEXT "Wartezeit zwischen zwei Elementen",IDC_STATIC,16,62,142, 8 EDITTEXT IDC_DELAY2_TEXT,165,79,40,14,ES_AUTOHSCROLL LTEXT "seconds",IDC_STATIC,207,82,28,8 CONTROL "Nur fr Ordner, die neue Nachrichten erhalten", IDC_INBOX_TIMER_ONLY,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,16,100,217,10 PUSHBUTTON "Datenordner zeigen",IDC_SHOW_DATA_FOLDER,7,238,70,14 CONTROL "Filtern im Hintergrund aktivieren", IDC_BUT_TIMER_ENABLED,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,16,12,162,10 PUSHBUTTON "Diagnose...",IDC_BUT_SHOW_DIAGNOSTICS,171,238,70,14 END IDD_STATISTICS DIALOG DISCARDABLE 0, 0, 248, 257 STYLE WS_CHILD | WS_CAPTION CAPTION "Statistik" FONT 8, "Tahoma" BEGIN GROUPBOX "Statistik",IDC_STATIC,7,3,241,229 LTEXT "some stats\nand some more\nline 3\nline 4\nline 5", IDC_STATISTICS,12,12,230,204 PUSHBUTTON "Statistik zurcksetzen",IDC_BUT_RESET_STATS,165,238,83, 14 LTEXT "Zuletzt zurckgesetzt:",IDC_STATIC,7,241,72,8 LTEXT "<<>>",IDC_LAST_RESET_DATE,84,241,70,8 END IDD_MANAGER DIALOGEX 0, 0, 275, 308 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "SpamBayes Manager" FONT 8, "Tahoma" BEGIN DEFPUSHBUTTON "Schlieen",IDOK,216,287,50,14 PUSHBUTTON "Abbrechen",IDCANCEL,155,287,50,14,NOT WS_VISIBLE CONTROL "",IDC_TAB,"SysTabControl32",0x0,8,7,258,276 PUSHBUTTON "ber...",IDC_ABOUT_BTN,8,287,50,14 END IDD_DIAGNOSTIC DIALOGEX 0, 0, 183, 98 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Diagnose" FONT 8, "Tahoma" BEGIN LTEXT "Diese erweiterten Optionen sind nur fr die Fehlersuche gedacht. Sie sollten hier nur Werte ndern, wenn Sie dazu aufgefordert wurden oder wenn Sie genau wissen, was sie bedeuten.", IDC_STATIC,5,3,174,36 LTEXT "Ausfhrlichkeit Logdatei",IDC_STATIC,5,44,77,8 EDITTEXT IDC_VERBOSE_LOG,84,42,31,14,ES_AUTOHSCROLL PUSHBUTTON "Log ansehen...",IDC_BUT_VIEW_LOG,117,41,62,14 CONTROL "Spamwert sichern",IDC_SAVE_SPAM_SCORE,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,5,63,72,10 PUSHBUTTON "Abbrechen",IDCANCEL,69,79,50,14,NOT WS_VISIBLE DEFPUSHBUTTON "Schlieen",IDOK,129,79,50,14 END IDD_FILTER DIALOGEX 0, 0, 249, 257 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP CAPTION "Filtern" FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Die folgenden Ordner filtern beim Eintreffen neuer Nachrichten", IDC_STATIC,8,4,207,11 CONTROL "Folder names...\nLine 2",IDC_FOLDER_WATCH,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,7,16,177,12 PUSHBUTTON "Durchsuchen",IDC_BROWSE_WATCH,192,14,50,14 GROUPBOX "Zweifelsfrei Spam",IDC_STATIC,7,31,235,82 LTEXT "Um sicher Spam zu sein, muss der Spamwert mindestens betragen:", IDC_STATIC,12,40,225,10 CONTROL "Slider1",IDC_SLIDER_CERTAIN,"msctls_trackbar32", TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,13,50,165,22 EDITTEXT IDC_EDIT_CERTAIN,184,53,51,14,ES_AUTOHSCROLL LTEXT "und folgende Aktion soll mit dieser Nachricht durchgefhrt werden:", IDC_STATIC,13,72,223,10 COMBOBOX IDC_ACTION_CERTAIN,12,83,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "in Ordner",IDC_STATIC,71,85,31,10 CONTROL "Ordner Namen",IDC_FOLDER_CERTAIN,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,103,83,77,14 PUSHBUTTON "Durchsuchen",IDC_BROWSE_CERTAIN,184,83,50,14 GROUPBOX "Mglicherweise Spam",IDC_STATIC,6,117,235,84 LTEXT "Um als unsicher gelten, muss der Spamwert mindestens betragen:", IDC_STATIC,12,128,212,10 CONTROL "Slider1",IDC_SLIDER_UNSURE,"msctls_trackbar32", TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,12,137,165,20 EDITTEXT IDC_EDIT_UNSURE,183,141,54,14,ES_AUTOHSCROLL LTEXT "und folgende Aktion soll mit dieser Nachricht durchgefhrt werden:", IDC_STATIC,12,158,217,10 COMBOBOX IDC_ACTION_UNSURE,12,169,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "in Ordner",IDC_STATIC,71,172,31,10 CONTROL "(folder name)",IDC_FOLDER_UNSURE,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,103,169,77,14 PUSHBUTTON "Durchsuchen",IDC_BROWSE_UNSURE,184,169,50,14 CONTROL "Spam als gelesen markieren",IDC_MARK_SPAM_AS_READ, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,100,154,10 CONTROL "Mglichen Spam als gelesen markieren", IDC_MARK_UNSURE_AS_READ,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,189,190,10 GROUPBOX "Sicher gut",IDC_STATIC,6,206,235,48 LTEXT "Aktion fr gute Nachrichten:",IDC_STATIC,12,218,107,10 COMBOBOX IDC_ACTION_HAM,12,231,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "in Ordner",IDC_STATIC,71,233,31,10 CONTROL "(folder name)",IDC_FOLDER_HAM,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,103,231,77,14 PUSHBUTTON "Durchsuchen",IDC_BROWSE_HAM,184,231,50,14 END IDD_GENERAL DIALOGEX 0, 0, 253, 257 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP CAPTION "Allgemein" FONT 8, "Tahoma" BEGIN LTEXT "SpamBayes Version Here",IDC_VERSION,6,54,242,8 LTEXT "SpamBayes bentigt Training, bevor es effektiv arbeiten kann. Klicken Sie auf die Registerkarte Training, um das Training durchzufhren.", IDC_STATIC,6,67,242,17 LTEXT "Status der Training Datenbank",IDC_STATIC,6,90,222,8 LTEXT "123 spam messages; 456 good messages\r\nLine2\r\nLine3", IDC_TRAINING_STATUS,6,101,242,27,SS_SUNKEN CONTROL "SpamBayes aktivieren",IDC_BUT_FILTER_ENABLE,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,6,221,97,11 LTEXT "Certain spam is moved to Folder1\nPossible spam is moved too", IDC_FILTER_STATUS,6,146,242,67,SS_SUNKEN PUSHBUTTON "Konfiguration zurcksetzen...",IDC_BUT_RESET,6,238,108, 15 PUSHBUTTON "Konfigurationsassistent...",IDC_BUT_WIZARD,155,238,93, 15 LTEXT "Filter Status:",IDC_STATIC,6,135,222,8 CONTROL 1062,IDC_LOGO_GRAPHIC,"Static",SS_BITMAP | SS_REALSIZEIMAGE,0,2,275,52 END IDD_TRAINING DIALOGEX 0, 0, 252, 257 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP CAPTION "Training" FONT 8, "Tahoma" BEGIN GROUPBOX "",IDC_STATIC,5,1,243,113 CONTROL "Ordner mit bekanntermaen guten Nachrichten",IDC_STATIC, "Static",SS_LEFTNOWORDWRAP | WS_GROUP,11,11,170,11 CONTROL "",IDC_STATIC_HAM,"Static",SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN,11,21,175,12 PUSHBUTTON "Durchsuchen",IDC_BROWSE_HAM,192,20,50,14 LTEXT "Ordner mit Spam oder anderen Mllnachrichten", IDC_STATIC,11,36,171,9 CONTROL "Static",IDC_STATIC_SPAM,"Static",SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,11,46,174,12 PUSHBUTTON "Durchsuchen",IDC_BROWSE_SPAM,192,46,50,14 CONTROL "Nachrichten nach Training bewerten",IDC_BUT_RESCORE, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,11,64,131,10 CONTROL "Datenbank komplett neu",IDC_BUT_REBUILD,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,147,64,94,10 CONTROL "Progress1",IDC_PROGRESS,"msctls_progress32",WS_BORDER, 11,76,231,11 PUSHBUTTON "Training &starten",IDC_START,11,91,54,14,BS_NOTIFY LTEXT "training status training status training status training status training status training status training status ", IDC_PROGRESS_TEXT,75,89,149,17 GROUPBOX "InkrementellesTraining",IDC_STATIC,4,117,244,87 CONTROL "Trainieren, dass eine Nachricht 'gut' ist, wenn sie aus einem Spam-Ordner in den Posteingang verschoben wird", IDC_BUT_TRAIN_FROM_SPAM_FOLDER,"Button",BS_AUTOCHECKBOX | BS_MULTILINE | WS_TABSTOP,11,127,204,18 LTEXT "Klicken auf 'Kein Spam' soll die Nachricht...", IDC_STATIC,10,148,141,10 COMBOBOX IDC_RECOVER_RS,153,145,88,54,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP CONTROL "Trainieren, dass eine Nachricht Spam ist, wenn sie in den Spam-Ordner verschoben wird.", IDC_BUT_TRAIN_TO_SPAM_FOLDER,"Button",BS_AUTOCHECKBOX | BS_MULTILINE | WS_TABSTOP,11,163,204,16 LTEXT "Klicken auf 'Spam' soll die Nachricht...",IDC_STATIC,10, 183,140,10 COMBOBOX IDC_DEL_SPAM_RS,153,180,88,54,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP END IDD_FILTER_NOW DIALOGEX 0, 0, 244, 185 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Jetzt filtern" FONT 8, "Tahoma" BEGIN LTEXT "Die folgenden Ordner filtern",IDC_STATIC,8,9,168,11 CONTROL "Folder names...\nLine 2",IDC_FOLDER_NAMES,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN,7,20,172, 12 PUSHBUTTON "Durchsuchen",IDC_BROWSE,187,19,50,14 GROUPBOX "Filteraktionen",IDC_STATIC,7,38,230,40,WS_GROUP CONTROL "Alle Aktionen ausfhren",IDC_BUT_ACT_ALL,"Button", BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,15,49,126,10 CONTROL "Nachrichten bewerten, aber keine Aktionen ausfhren", IDC_BUT_ACT_SCORE,"Button",BS_AUTORADIOBUTTON,15,62,203, 10 GROUPBOX "Filter beschrnken",IDC_STATIC,7,84,230,35,WS_GROUP CONTROL "Nur ungelesene Nachrichten bearbeiten",IDC_BUT_UNREAD, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,15,94,149,9 CONTROL "Nur ungefilterte Nachrichten verarbeiten", IDC_BUT_UNSEEN,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,15, 106,149,9 CONTROL "Progress1",IDC_PROGRESS,"msctls_progress32",WS_BORDER,7, 129,230,11 LTEXT "Static",IDC_PROGRESS_TEXT,7,144,227,10 DEFPUSHBUTTON "Start filtern",IDC_START,7,161,52,14 PUSHBUTTON "Schlieen",IDCANCEL,187,162,50,14 END IDD_FOLDER_SELECTOR DIALOG DISCARDABLE 0, 0, 247, 215 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Ordner auswhlen" FONT 8, "Tahoma" BEGIN LTEXT "&Folders:",IDC_STATIC,7,7,47,9 CONTROL "",IDC_LIST_FOLDERS,"SysTreeView32",TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | TVS_DISABLEDRAGDROP | TVS_SHOWSELALWAYS | TVS_CHECKBOXES | WS_BORDER | WS_TABSTOP,7,21,172,140 CONTROL "(sub)",IDC_BUT_SEARCHSUB,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,7,167,126,9 LTEXT "(status1)",IDC_STATUS1,7,180,220,9 LTEXT "(status2)",IDC_STATUS2,7,194,220,9 DEFPUSHBUTTON "OK",IDOK,190,21,50,14 PUSHBUTTON "Abbrechen",IDCANCEL,190,39,50,14 PUSHBUTTON "Alle lschen",IDC_BUT_CLEARALL,190,58,50,14 PUSHBUTTON "Neuer Ordner",IDC_BUT_NEW,190,77,50,14 END IDD_WIZARD DIALOGEX 0, 0, 384, 190 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "SpamBayes Konfigurationsassistent" FONT 8, "Tahoma" BEGIN PUSHBUTTON "Abbrechen",IDCANCEL,328,173,50,14 PUSHBUTTON "<< Zurck",IDC_BACK_BTN,216,173,50,14 DEFPUSHBUTTON "Weiter >>,Beenden",IDC_FORWARD_BTN,269,173,50,14 CONTROL "",IDC_PAGE_PLACEHOLDER,"Static",SS_ETCHEDFRAME,75,4,303, 167 CONTROL 125,IDC_WIZ_GRAPHIC,"Static",SS_BITMAP,0,0,69,190 END IDD_WIZARD_WELCOME DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma" BEGIN LTEXT "Willkommen zum SpamBayes Konfigurationsassistenten", IDC_STATIC,20,4,191,14 LTEXT "Dieser Assistent hilft Ihnen, SpamBayes einzurichten. Bitte geben Sie an, wie Sie sich auf den Umgang mit dem Programm vorbereitet haben.", IDC_STATIC,20,20,255,18 CONTROL "Ich habe gar nichts vorbereitet.",IDC_BUT_PREPARATION, "Button",BS_AUTORADIOBUTTON | BS_TOP | WS_GROUP,20,42, 190,11 CONTROL "Ich habe bereits Spam und 'gute' Nachrichten (Ham) in fr das Training geeignete Ordner sortiert.", IDC_STATIC,"Button",BS_AUTORADIOBUTTON | BS_TOP | BS_MULTILINE,20,59,255,18 CONTROL "Ich bevorzuge, SpamBayes manuell zu konfigurieren (Expertenmodus)", IDC_STATIC,"Button",BS_AUTORADIOBUTTON | BS_TOP,20,82, 255,12 LTEXT "Wenn Sie mehr ber die Konfiguration und das Training von SpamBayes erfahren mchten, drcken Sie den Knopf 'ber...'", IDC_STATIC,20,103,206,22 PUSHBUTTON "ber...",IDC_BUT_ABOUT,233,104,42,15 LTEXT "Wenn Sie den SpamBayes Konfigurationsassistenten abbrechen, knnen Sie ihn jederzeit ber den SpamBayes Manager von der Outlook Symbolleiste neu starten.", IDC_STATIC,20,129,247,26 END IDD_WIZARD_FINISHED_UNTRAINED DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma" BEGIN LTEXT "Gratulation",IDC_STATIC,20,4,247,14 LTEXT "SpamBayes ist jetzt konfiguriert und bereit, ber ihre Nachrichten zu lernen.", IDC_STATIC,20,22,247,16 LTEXT "Weil SpamBayes nicht trainiert ist, landen alle Nachrichten im Ordner 'unsicher'. Bitte benutzen Sie die Schaltflchen 'Spam' und 'Kein Spam', um SpamBayes zu trainieren.", IDC_STATIC,20,42,247,27 LTEXT "Wenn Sie die Lernzeit verkrzen wollen, verschieben Sie allen vorhandenen Spam in einen Ordner und trainieren danach SpamBayes mit Hilfe des SpamBayes Managers.", IDC_STATIC,20,94,247,31 LTEXT "Wenn Sie SpamBayes auf diese Weise trainieren, werden Sie feststellen, dass die Genauigkeit von SpamBayes zunimmt.", IDC_STATIC,20,69,247,18 LTEXT "Klicken Sie 'Beenden', um den Assistenten zu schlieen.", IDC_STATIC,20,132,263,9 END IDD_WIZARD_FOLDERS_REST DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma" BEGIN PUSHBUTTON "Durchsuchen",IDC_BROWSE_SPAM,208,100,60,15 LTEXT "Ordner fr Spam und 'unsichere' Nachrichten",IDC_STATIC, 20,4,247,14 LTEXT "SpamBayes benutzt zwei Ordner, um Spam zu verwalten. Einen Ordner, der Nachrichten enthlt, bei denen sich SpamBayes sicher ist und einen, wo es unsicher ist.", IDC_STATIC,20,20,247,29 LTEXT "Wenn Sie einen Ordnernamen eingeben, der nicht existiert, wird ein Ordner mit diesem Namen erstellt. Sollten Sie einen bereits existierenden Ordner bevorzugen, klicken Sie auf 'Durchsuchen', um den Ordner auszuwhlen.", IDC_STATIC,20,53,243,24 EDITTEXT IDC_FOLDER_CERTAIN,20,100,179,14,ES_AUTOHSCROLL LTEXT "Unsichere Nachrichten kommen in folgenden Ordner:", IDC_STATIC,20,121,227,12 EDITTEXT IDC_FOLDER_UNSURE,20,132,177,14,ES_AUTOHSCROLL LTEXT "Spam soll in folgenden Ordner zugestellt werden:", IDC_STATIC,20,89,189,8 PUSHBUTTON "Durchsuchen",IDC_BROWSE_UNSURE,208,132,60,15 END IDD_WIZARD_FOLDERS_WATCH DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma" BEGIN PUSHBUTTON "Durchsuchen",IDC_BROWSE_WATCH,225,134,50,14 LTEXT "Ordner, in denen neue Nachrichten eintreffen", IDC_STATIC,20,4,247,14 LTEXT "SpamBayes muss wissen, in welchen Ordnern neue Nachrichten eintreffen. In den meisen Fllen ist dies der Posteingang. Sie knnen aber weitere Ordner angeben, die von SpamBayes berwacht werden sollen.", IDC_STATIC,20,21,247,25 LTEXT "Die folgende Liste enthlt die zu beobachtenden Ordner. Drcken Sie auf 'Durchsuchen', um die Liste zu ndern, bzw. auf 'Weiter', um fortzufahren.", IDC_STATIC,20,79,247,20 LTEXT "Wenn Sie den Outlook Regelassistenten benutzen, um Nachrichten zu verschieben, knnen Sie solche Ordner zustzlich angeben.", IDC_STATIC,20,51,241,20 EDITTEXT IDC_FOLDER_WATCH,20,100,195,48,ES_MULTILINE | ES_AUTOHSCROLL | ES_READONLY END IDD_WIZARD_FINISHED_UNCONFIGURED DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma" BEGIN LTEXT "Konfiguration abgebrochen",IDC_STATIC,20,4,247,14 LTEXT "Die SpamBayes Optionen werden jetzt angezeigt. Sie mssen Ihre Ordner auswhlen, bevor SpamBayes beginnt, Nachrichten zu filtern.", IDC_STATIC,20,29,247,16 LTEXT "Klicken Sie auf 'Beenden', um den Assistenten zu beenden.", IDC_STATIC,20,139,240,16 END IDD_WIZARD_FOLDERS_TRAIN DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma" BEGIN PUSHBUTTON "Druchsuchen",IDC_BROWSE_HAM,208,49,60,15 LTEXT "Training",IDC_STATIC,20,4,247,10 LTEXT "Bitte whlen Sie die Nachrichten mit dem vorsortierten Spam und den vorsortierten 'guten' Nachrichten.", IDC_STATIC,20,16,243,16 EDITTEXT IDC_FOLDER_HAM,20,49,179,14,ES_AUTOHSCROLL | ES_READONLY LTEXT "Beispiele von Spam und anderer unerwnschter Nachrichten finden sich hier:", IDC_STATIC,20,71,248,8 EDITTEXT IDC_FOLDER_CERTAIN,20,81,177,14,ES_AUTOHSCROLL | ES_READONLY LTEXT "Beispiele 'guter' Nachrichten finden sich unter", IDC_STATIC,20,38,153,8 PUSHBUTTON "Durchsuchen",IDC_BROWSE_SPAM,208,81,60,15 LTEXT "Wenn Sie keine vorsortierten Nachrichten haben oder bereits vorhandene SpamBayes-Daten weiter benutzen mchten, gehen Sie bitte zurck und geben an, dass Sie sich nicht vorbereitet haben.", IDC_STATIC,20,128,243,26 CONTROL "Nachrichten nach dem Training bewerten",IDC_BUT_RESCORE, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,20,108,163,16 END IDD_WIZARD_TRAIN DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma" BEGIN LTEXT "Training",-1,20,4,247,14 LTEXT "SpamBayes wird trainiert anhand Ihrer guten Nachrichten und Ihres Spams", -1,20,22,247,16 CONTROL "",IDC_PROGRESS,"msctls_progress32",WS_BORDER,20,45,255, 11 LTEXT "(progress text)",IDC_PROGRESS_TEXT,20,61,257,10 END IDD_WIZARD_FINISHED_TRAINED DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma" BEGIN LTEXT "Gratulation",IDC_STATIC,20,4,247,14 LTEXT "SpamBayes wurde erfolgreich trainiert und konfiguriert. SpamBayes sollte jetzt bereit sein, die Nachrichten effektiv zu filtern.", IDC_TRAINING_STATUS,20,35,247,26 LTEXT "Obwohl SpamBayes jetzt erfolgreich trainiert wurde, lernt SpamBayes weiter. Bitte schauen Sie regelmig in den Ordner mit den 'unsicheren' Nachrichten und benutzen die Schaltflchen 'Spam' und 'Kein Spam'.", IDC_STATIC,20,68,249,30 LTEXT "Klicken Sie auf Beenden, um den Assistenten zu schlieen.", IDC_STATIC,20,104,257,23 END IDD_WIZARD_TRAINING_IS_IMPORTANT DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma" BEGIN LTEXT "SpamBayes kann nicht effektiv arbeiten, wenn es untrainiert ist.", IDC_STATIC,11,8,263,11 PUSHBUTTON "Training...",IDC_BUT_ABOUT,225,140,49,15 LTEXT "SpamBayes besitzt keine vordefinierten Regeln sondern lernt von Ihnen, Spam von 'guten' Nachrichten (Ham) zu unterscheiden. Sie mssen SpamBayes deshalb Ordner mit guten und schlechten Nachrichten zum Training zur Verfgung stellen.", IDC_STATIC,11,21,263,30 LTEXT "In diesem Fall stellt SpamBayes anfangs alle Nachrichten in den Ordner 'unsicher'. Whrend Sie dann mit den Knpfen 'Spam' und 'Kein Spam' die Nachrichten zuordnen, lernt SpamBayes den Umgang mit Ihren Nachrichten.", IDC_STATIC,22,61,252,29 LTEXT "Diese Option wird den Assistenten beenden und erklren, wie Sie Ihre Nachrichten vorsortieren knnen. Danach knnen Sie SpamBayes trainieren und SpamBayes wird sofort beginnen, effektiv zu arbeiten.", IDC_STATIC,22,106,252,27 LTEXT "Fr mehr Informationen bettigen Sie bitte den Knopf 'Training...'", IDC_STATIC,11,143,211,12 CONTROL "Ich mchte ohne Training fortfahren",IDC_BUT_UNTRAINED, "Button",BS_AUTORADIOBUTTON | WS_GROUP,11,50,263,11 CONTROL "Ich werde die Nachrichten vorsortieren und SpamBayes danach konfigurieren.", IDC_BUT_TRAIN,"Button",BS_AUTORADIOBUTTON,11,92,263,11 END IDD_WIZARD_FINISHED_TRAIN_LATER DIALOGEX 0, 0, 284, 162 STYLE WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma" BEGIN LTEXT "Konfiguration angehalten",IDC_STATIC,20,4,247,14 LTEXT "Um mit dem Training zu beginnen, sollten Sie einen Ordner erstellen, der nur Beispiele von 'guten' Nachrichten enthlt und einen, der nur Beispiele von Spam enthlt.", IDC_STATIC,20,17,257,27 LTEXT "Klicken Sie auf 'Beenden', um den Assistenten zu schlieen.", IDC_STATIC,20,145,228,9 LTEXT "Fr Beispiele von 'guten' Nachrichten knnen Sie z.B. den Posteingang benutzen. Es ist aber wichtig, aus diesem erst den gesamten Spam zu entfernen, bevor Sie fortfahren. Wenn Sie zuviel in Ihrem Posteingang haben, knnen Sie auch einen Teil davon in einen temporren Ordner kopieren.", IDC_STATIC,20,44,256,36 LTEXT "Fr Beispiele von Spam knnen Sie z.B. im Posteingang oder in 'Gelschte Objekte' suchen. SpamBayes erlaubt jedoch nicht, den Ordner 'Gelschte Objekte' selbst anzugeben. Sie knnen jedoch die Elemente aus 'Gelschte Objekte' in einen selbst angelegten Ordner kopieren.", IDC_STATIC,20,80,247,35 LTEXT "Wenn Sie fertig damit sind, starten Sie den SpamBayes Installationsassistenten erneut und konfigurieren Sie SpamBayes.", IDC_STATIC,20,121,257,17 END IDD_NOTIFICATIONS DIALOG DISCARDABLE 0, 0, 248, 257 STYLE WS_CHILD | WS_CAPTION CAPTION "Notifizierung" FONT 8, "Tahoma" BEGIN GROUPBOX "Klnge fr neue Nachrichten",IDC_STATIC,7,3,241,229 CONTROL "Klnge fr neue Nachrichten aktivieren", IDC_ENABLE_SOUNDS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP, 14,17,140,10 LTEXT "Gute Nachricht:",IDC_STATIC,14,31,51,8 EDITTEXT IDC_HAM_SOUND,14,40,174,14,ES_AUTOHSCROLL PUSHBUTTON "Durchsuchen",IDC_BROWSE_HAM_SOUND,192,40,50,14 LTEXT "Unsichere Nachricht:",IDC_STATIC,14,58,67,8 EDITTEXT IDC_UNSURE_SOUND,14,67,174,14,ES_AUTOHSCROLL PUSHBUTTON "Durchsuchen",IDC_BROWSE_UNSURE_SOUND,192,67,50,14 LTEXT "Spam:",IDC_STATIC,14,85,21,8 EDITTEXT IDC_SPAM_SOUND,14,94,174,14,ES_AUTOHSCROLL PUSHBUTTON "Durchsuchen",IDC_BROWSE_SPAM_SOUND,192,94,50,14 LTEXT "Zeit, um auf weitere Nachrichten zu warten",IDC_STATIC, 14,116,142,8 CONTROL "",IDC_ACCUMULATE_DELAY_SLIDER,"msctls_trackbar32", TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,14,127,148,22 EDITTEXT IDC_ACCUMULATE_DELAY_TEXT,163,133,40,14,ES_AUTOHSCROLL LTEXT "Sekunden",IDC_STATIC,205,136,35,8 END ///////////////////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO DISCARDABLE BEGIN IDD_ADVANCED, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 241 VERTGUIDE, 16 BOTTOMMARGIN, 204 END IDD_MANAGER, DIALOG BEGIN BOTTOMMARGIN, 253 END IDD_DIAGNOSTIC, DIALOG BEGIN LEFTMARGIN, 5 RIGHTMARGIN, 179 BOTTOMMARGIN, 93 END IDD_FILTER, DIALOG BEGIN TOPMARGIN, 4 BOTTOMMARGIN, 254 HORZGUIDE, 127 END IDD_GENERAL, DIALOG BEGIN RIGHTMARGIN, 248 VERTGUIDE, 6 BOTTOMMARGIN, 205 END IDD_TRAINING, DIALOG BEGIN RIGHTMARGIN, 241 VERTGUIDE, 11 VERTGUIDE, 242 BOTTOMMARGIN, 207 END IDD_FILTER_NOW, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 237 TOPMARGIN, 9 BOTTOMMARGIN, 176 END IDD_WIZARD, DIALOG BEGIN RIGHTMARGIN, 378 END IDD_WIZARD_WELCOME, DIALOG BEGIN VERTGUIDE, 20 VERTGUIDE, 275 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_UNTRAINED, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FOLDERS_REST, DIALOG BEGIN VERTGUIDE, 20 VERTGUIDE, 268 BOTTOMMARGIN, 161 HORZGUIDE, 85 HORZGUIDE, 117 END IDD_WIZARD_FOLDERS_WATCH, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_UNCONFIGURED, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FOLDERS_TRAIN, DIALOG BEGIN VERTGUIDE, 20 VERTGUIDE, 268 BOTTOMMARGIN, 161 HORZGUIDE, 49 HORZGUIDE, 81 END IDD_WIZARD_TRAIN, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_TRAINED, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_TRAINING_IS_IMPORTANT, DIALOG BEGIN VERTGUIDE, 11 VERTGUIDE, 22 VERTGUIDE, 274 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_TRAIN_LATER, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_NOTIFICATIONS, DIALOG BEGIN LEFTMARGIN, 7 TOPMARGIN, 7 BOTTOMMARGIN, 232 END END #endif // APSTUDIO_INVOKED #endif // Deutsch (Deutschland) resources ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// // Englisch (USA) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Bitmap // IDB_SBLOGO BITMAP MOVEABLE PURE "sblogo.bmp" IDB_SBWIZLOGO BITMAP MOVEABLE PURE "sbwizlogo.bmp" IDB_FOLDERS BITMAP MOVEABLE PURE "folders.bmp" #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE MOVEABLE PURE BEGIN "dialogs.h\0" END 2 TEXTINCLUDE MOVEABLE PURE BEGIN "#include ""winres.h""\r\n" "// spambayes dialog definitions\r\n" "\0" END 3 TEXTINCLUDE MOVEABLE PURE BEGIN "\0" END #endif // APSTUDIO_INVOKED #endif // Englisch (USA) resources ///////////////////////////////////////////////////////////////////////////// spambayes-1.1a6/spambayes/languages/de/DIALOGS/i18n_dialogs.py0000664000076500000240000006336010646440126024133 0ustar skipstaff00000000000000#i18n_dialogs.py #This is a generated file. Please edit dialogs.rc instead. _rc_size_=30899 _rc_mtime_=1173774820 try: _ except NameError: def _(s): return s class FakeParser: dialogs = {'IDD_WIZARD_FINISHED_TRAIN_LATER': [['', (0, 0, 284, 162), 1354760256, 1024, (8, 'Tahoma')], [130, _('Konfiguration angehalten'), -1, (20, 4, 247, 14), 1342177280], [130, _("Um mit dem Training zu beginnen, sollten Sie einen Ordner erstellen, der nur Beispiele von 'guten' Nachrichten enth\xe4lt und einen, der nur Beispiele von Spam enth\xe4lt."), -1, (20, 17, 257, 27), 1342177280], [130, _("Klicken Sie auf 'Beenden', um den Assistenten zu schlie\xdfen."), -1, (20, 145, 228, 9), 1342177280], [130, _("F\xfcr Beispiele von 'guten' Nachrichten k\xf6nnen Sie z.B. den Posteingang benutzen. Es ist aber wichtig, aus diesem erst den gesamten Spam zu entfernen, bevor Sie fortfahren. Wenn Sie zuviel in Ihrem Posteingang haben, k\xf6nnen Sie auch einen Teil davon in einen tempor\xe4ren Ordner kopieren."), -1, (20, 44, 256, 36), 1342177280], [130, _("F\xfcr Beispiele von Spam k\xf6nnen Sie z.B. im Posteingang oder in 'Gel\xf6schte Objekte' suchen. SpamBayes erlaubt jedoch nicht, den Ordner 'Gel\xf6schte Objekte' selbst anzugeben. Sie k\xf6nnen jedoch die Elemente aus 'Gel\xf6schte Objekte' in einen selbst angelegten Ordner kopieren."), -1, (20, 80, 247, 35), 1342177280], [130, _('Wenn Sie fertig damit sind, starten Sie den SpamBayes Installationsassistenten erneut und konfigurieren Sie SpamBayes.'), -1, (20, 121, 257, 17), 1342177280]], 'IDD_WIZARD_WELCOME': [['', (0, 0, 284, 162), 1354760256, 1024, (8, 'Tahoma')], [130, _('Willkommen zum SpamBayes Konfigurationsassistenten'), -1, (20, 4, 191, 14), 1342177280], [130, _('Dieser Assistent hilft Ihnen, SpamBayes einzurichten. Bitte geben Sie an, wie Sie sich auf den Umgang mit dem Programm vorbereitet haben.'), -1, (20, 20, 255, 18), 1342177280], [128, _('Ich habe gar nichts vorbereitet.'), 1081, (20, 42, 190, 11), 1342309385], [128, _("Ich habe bereits Spam und 'gute' Nachrichten (Ham) in f\xfcr das Training geeignete Ordner sortiert."), -1, (20, 59, 255, 18), 1342186505], [128, _('Ich bevorzuge, SpamBayes manuell zu konfigurieren (Expertenmodus)'), -1, (20, 82, 255, 12), 1342178313], [130, _("Wenn Sie mehr \xfcber die Konfiguration und das Training von SpamBayes erfahren m\xf6chten, dr\xfccken Sie den Knopf '\xdcber...'"), -1, (20, 103, 206, 22), 1342177280], [128, _('\xdcber...'), 1017, (233, 104, 42, 15), 1342177280], [130, _('Wenn Sie den SpamBayes Konfigurationsassistenten abbrechen, k\xf6nnen Sie ihn jederzeit \xfcber den SpamBayes Manager von der Outlook Symbolleiste neu starten.'), -1, (20, 129, 247, 26), 1342177280]], 'IDD_WIZARD': [[_('SpamBayes Konfigurationsassistent'), (0, 0, 384, 190), -1865940800, 1024, (8, 'Tahoma')], [128, _('Abbrechen'), 2, (328, 173, 50, 14), 1342177280], [128, _('<< Zur\xfcck'), 1069, (216, 173, 50, 14), 1342177280], [128, _('Weiter >>,Beenden'), 1077, (269, 173, 50, 14), 1342177281], [130, '', 1078, (75, 4, 303, 167), 1342177298], [130, '125', 1092, (0, 0, 69, 190), 1342177294]], 'IDD_WIZARD_FINISHED_UNTRAINED': [['', (0, 0, 284, 162), 1354760256, 1024, (8, 'Tahoma')], [130, _('Gratulation'), -1, (20, 4, 247, 14), 1342177280], [130, _('SpamBayes ist jetzt konfiguriert und bereit, \xfcber ihre Nachrichten zu lernen.'), -1, (20, 22, 247, 16), 1342177280], [130, _("Weil SpamBayes nicht trainiert ist, landen alle Nachrichten im Ordner 'unsicher'. Bitte benutzen Sie die Schaltfl\xe4chen 'Spam' und 'Kein Spam', um SpamBayes zu trainieren."), -1, (20, 42, 247, 27), 1342177280], [130, _('Wenn Sie die Lernzeit verk\xfcrzen wollen, verschieben Sie allen vorhandenen Spam in einen Ordner und trainieren danach SpamBayes mit Hilfe des SpamBayes Managers.'), -1, (20, 94, 247, 31), 1342177280], [130, _('Wenn Sie SpamBayes auf diese Weise trainieren, werden Sie feststellen, dass die Genauigkeit von SpamBayes zunimmt.'), -1, (20, 69, 247, 18), 1342177280], [130, _("Klicken Sie 'Beenden', um den Assistenten zu schlie\xdfen."), -1, (20, 132, 263, 9), 1342177280]], 'IDD_GENERAL': [[_('Allgemein'), (0, 0, 253, 257), 1354760256, 1024, (8, 'Tahoma')], [130, _('SpamBayes Version Here'), 1009, (6, 54, 242, 8), 1342177280], [130, _('SpamBayes ben\xf6tigt Training, bevor es effektiv arbeiten kann. Klicken Sie auf die Registerkarte Training, um das Training durchzuf\xfchren.'), -1, (6, 67, 242, 17), 1342177280], [130, _('Status der Training Datenbank'), -1, (6, 90, 222, 8), 1342177280], [130, _('123 spam messages; 456 good messages\\r\\nLine2\\r\\nLine3'), 1035, (6, 101, 242, 27), 1342181376], [128, _('SpamBayes aktivieren'), 1013, (6, 221, 97, 11), 1342242819], [130, _('Certain spam is moved to Folder1\\nPossible spam is moved too'), 1014, (6, 146, 242, 67), 1342181376], [128, _('Konfiguration zur\xfccksetzen...'), 1073, (6, 238, 108, 15), 1342177280], [128, _('Konfigurationsassistent...'), 1070, (155, 238, 93, 15), 1342177280], [130, _('Filter Status:'), -1, (6, 135, 222, 8), 1342177280], [130, '1062', 1063, (0, 2, 275, 52), 1342179342]], 'IDD_WIZARD_FINISHED_UNCONFIGURED': [['', (0, 0, 284, 162), 1354760256, 1024, (8, 'Tahoma')], [130, _('Konfiguration abgebrochen'), -1, (20, 4, 247, 14), 1342177280], [130, _('Die SpamBayes Optionen werden jetzt angezeigt. Sie m\xfcssen Ihre Ordner ausw\xe4hlen, bevor SpamBayes beginnt, Nachrichten zu filtern.'), -1, (20, 29, 247, 16), 1342177280], [130, _("Klicken Sie auf 'Beenden', um den Assistenten zu beenden."), -1, (20, 139, 240, 16), 1342177280]], 'IDD_MANAGER': [[_('SpamBayes Manager'), (0, 0, 275, 308), -1865940800, 1024, (8, 'Tahoma')], [128, _('Schlie\xdfen'), 1, (216, 287, 50, 14), 1342177281], [128, _('Abbrechen'), 2, (155, 287, 50, 14), 1073741824], ['SysTabControl32', '', 1068, (8, 7, 258, 276), 1342177280], [128, _('\xdcber...'), 1072, (8, 287, 50, 14), 1342177280]], 'IDD_WIZARD_FOLDERS_REST': [['', (0, 0, 284, 162), 1354760256, 1024, (8, 'Tahoma')], [128, _('Durchsuchen'), 1005, (208, 100, 60, 15), 1342177280], [130, _("Ordner f\xfcr Spam und 'unsichere' Nachrichten"), -1, (20, 4, 247, 14), 1342177280], [130, _('SpamBayes benutzt zwei Ordner, um Spam zu verwalten. Einen Ordner, der Nachrichten enth\xe4lt, bei denen sich SpamBayes sicher ist und einen, wo es unsicher ist.'), -1, (20, 20, 247, 29), 1342177280], [130, _("Wenn Sie einen Ordnernamen eingeben, der nicht existiert, wird ein Ordner mit diesem Namen erstellt. Sollten Sie einen bereits existierenden Ordner bevorzugen, klicken Sie auf 'Durchsuchen', um den Ordner auszuw\xe4hlen."), -1, (20, 53, 243, 24), 1342177280], [129, '', 1027, (20, 100, 179, 14), 1350566016], [130, _('Unsichere Nachrichten kommen in folgenden Ordner:'), -1, (20, 121, 227, 12), 1342177280], [129, '', 1033, (20, 132, 177, 14), 1350566016], [130, _('Spam soll in folgenden Ordner zugestellt werden:'), -1, (20, 89, 189, 8), 1342177280], [128, _('Durchsuchen'), 1034, (208, 132, 60, 15), 1342177280]], 'IDD_WIZARD_TRAIN': [['', (0, 0, 284, 162), 1354760256, 1024, (8, 'Tahoma')], [130, _('Training'), -1, (20, 4, 247, 14), 1342177280], [130, _('SpamBayes wird trainiert anhand Ihrer guten Nachrichten und Ihres Spams'), -1, (20, 22, 247, 16), 1342177280], ['msctls_progress32', '', 1000, (20, 45, 255, 11), 1350565888], [130, _('(progress text)'), 1001, (20, 61, 257, 10), 1342177280]], 'IDD_DIAGNOSTIC': [[_('Diagnose'), (0, 0, 183, 98), -1865940800, 1024, (8, 'Tahoma')], [130, _('Diese erweiterten Optionen sind nur f\xfcr die Fehlersuche gedacht. Sie sollten hier nur Werte \xe4ndern, wenn Sie dazu aufgefordert wurden oder wenn Sie genau wissen, was sie bedeuten.'), -1, (5, 3, 174, 36), 1342177280], [130, _('Ausf\xfchrlichkeit Logdatei'), -1, (5, 44, 77, 8), 1342177280], [129, '', 1061, (84, 42, 31, 14), 1350566016], [128, _('Log ansehen...'), 1093, (117, 41, 62, 14), 1342177280], [128, _('Spamwert sichern'), 1048, (5, 63, 72, 10), 1342242819], [128, _('Abbrechen'), 2, (69, 79, 50, 14), 1073741824], [128, _('Schlie\xdfen'), 1, (129, 79, 50, 14), 1342177281]], 'IDD_FILTER': [[_('Filtern'), (0, 0, 249, 257), 1354760256, 1024, (8, 'Tahoma')], [130, _('Die folgenden Ordner filtern beim Eintreffen neuer Nachrichten'), -1, (8, 4, 207, 11), 1342177280], [130, _('Folder names...\\nLine 2'), 1038, (7, 16, 177, 12), 1342312972], [128, _('Durchsuchen'), 1039, (192, 14, 50, 14), 1342177280], [128, _('Zweifelsfrei Spam'), -1, (7, 31, 235, 82), 1342177287], [130, _('Um sicher Spam zu sein, muss der Spamwert mindestens betragen:'), -1, (12, 40, 225, 10), 1342177280], ['msctls_trackbar32', _('Slider1'), 1023, (13, 50, 165, 22), 1342242821], [129, '', 1024, (184, 53, 51, 14), 1350566016], [130, _('und folgende Aktion soll mit dieser Nachricht durchgef\xfchrt werden:'), -1, (13, 72, 223, 10), 1342177280], [133, '', 1025, (12, 83, 55, 40), 1344339971], [130, _('in Ordner'), -1, (71, 85, 31, 10), 1342177280], [130, _('Ordner Namen'), 1027, (103, 83, 77, 14), 1342312972], [128, _('Durchsuchen'), 1028, (184, 83, 50, 14), 1342177280], [128, _('M\xf6glicherweise Spam'), -1, (6, 117, 235, 84), 1342177287], [130, _('Um als unsicher gelten, muss der Spamwert mindestens betragen:'), -1, (12, 128, 212, 10), 1342177280], ['msctls_trackbar32', _('Slider1'), 1029, (12, 137, 165, 20), 1342242821], [129, '', 1030, (183, 141, 54, 14), 1350566016], [130, _('und folgende Aktion soll mit dieser Nachricht durchgef\xfchrt werden:'), -1, (12, 158, 217, 10), 1342177280], [133, '', 1031, (12, 169, 55, 40), 1344339971], [130, _('in Ordner'), -1, (71, 172, 31, 10), 1342177280], [130, _('(folder name)'), 1033, (103, 169, 77, 14), 1342312972], [128, _('Durchsuchen'), 1034, (184, 169, 50, 14), 1342177280], [128, _('Spam als gelesen markieren'), 1047, (13, 100, 154, 10), 1342242819], [128, _('M\xf6glichen Spam als gelesen markieren'), 1051, (12, 189, 190, 10), 1342242819], [128, _('Sicher gut'), -1, (6, 206, 235, 48), 1342177287], [130, _('Aktion f\xfcr gute Nachrichten:'), -1, (12, 218, 107, 10), 1342177280], [133, '', 1032, (12, 231, 55, 40), 1344339971], [130, _('in Ordner'), -1, (71, 233, 31, 10), 1342177280], [130, _('(folder name)'), 1083, (103, 231, 77, 14), 1342312972], [128, _('Durchsuchen'), 1004, (184, 231, 50, 14), 1342177280]], 'IDD_FILTER_NOW': [[_('Jetzt filtern'), (0, 0, 244, 185), -1865940800, 1024, (8, 'Tahoma')], [130, _('Die folgenden Ordner filtern'), -1, (8, 9, 168, 11), 1342177280], [130, _('Folder names...\\nLine 2'), 1036, (7, 20, 172, 12), 1342181900], [128, _('Durchsuchen'), 1037, (187, 19, 50, 14), 1342177280], [128, _('Filteraktionen'), -1, (7, 38, 230, 40), 1342308359], [128, _('Alle Aktionen ausf\xfchren'), 1019, (15, 49, 126, 10), 1342373897], [128, _('Nachrichten bewerten, aber keine Aktionen ausf\xfchren'), 1018, (15, 62, 203, 10), 1342177289], [128, _('Filter beschr\xe4nken'), -1, (7, 84, 230, 35), 1342308359], [128, _('Nur ungelesene Nachrichten bearbeiten'), 1020, (15, 94, 149, 9), 1342242819], [128, _('Nur ungefilterte Nachrichten verarbeiten'), 1021, (15, 106, 149, 9), 1342242819], ['msctls_progress32', _('Progress1'), 1000, (7, 129, 230, 11), 1350565888], [130, _('Static'), 1001, (7, 144, 227, 10), 1342177280], [128, _('Start filtern'), 1006, (7, 161, 52, 14), 1342177281], [128, _('Schlie\xdfen'), 2, (187, 162, 50, 14), 1342177280]], 'IDD_TRAINING': [[_('Training'), (0, 0, 252, 257), 1354760256, 1024, (8, 'Tahoma')], [128, '', -1, (5, 1, 243, 113), 1342177287], [130, _('Ordner mit bekannterma\xdfen guten Nachrichten'), -1, (11, 11, 170, 11), 1342308364], [130, '', 1002, (11, 21, 175, 12), 1342181900], [128, _('Durchsuchen'), 1004, (192, 20, 50, 14), 1342177280], [130, _('Ordner mit Spam oder anderen M\xfcllnachrichten'), -1, (11, 36, 171, 9), 1342177280], [130, _('Static'), 1003, (11, 46, 174, 12), 1342312972], [128, _('Durchsuchen'), 1005, (192, 46, 50, 14), 1342177280], [128, _('Nachrichten nach Training bewerten'), 1008, (11, 64, 131, 10), 1342242819], [128, _('Datenbank komplett neu'), 1007, (147, 64, 94, 10), 1342242819], ['msctls_progress32', _('Progress1'), 1000, (11, 76, 231, 11), 1350565888], [128, _('Training &starten'), 1006, (11, 91, 54, 14), 1342193664], [130, _('training status training status training status training status training status training status training status '), 1001, (75, 89, 149, 17), 1342177280], [128, _('InkrementellesTraining'), -1, (4, 117, 244, 87), 1342177287], [128, _("Trainieren, dass eine Nachricht 'gut' ist, wenn sie aus einem Spam-Ordner in den Posteingang verschoben wird"), 1010, (11, 127, 204, 18), 1342251011], [130, _("Klicken auf 'Kein Spam' soll die Nachricht..."), -1, (10, 148, 141, 10), 1342177280], [133, '', 1075, (153, 145, 88, 54), 1344339971], [128, _('Trainieren, dass eine Nachricht Spam ist, wenn sie in den Spam-Ordner verschoben wird.'), 1011, (11, 163, 204, 16), 1342251011], [130, _("Klicken auf 'Spam' soll die Nachricht..."), -1, (10, 183, 140, 10), 1342177280], [133, '', 1074, (153, 180, 88, 54), 1344339971]], 'IDD_NOTIFICATIONS': [[_('Notifizierung'), (0, 0, 248, 257), 1354760256, None, (8, 'Tahoma')], [128, _('Kl\xe4nge f\xfcr neue Nachrichten'), -1, (7, 3, 241, 229), 1342177287], [128, _('Kl\xe4nge f\xfcr neue Nachrichten aktivieren'), 1098, (14, 17, 140, 10), 1342242819], [130, _('Gute Nachricht:'), -1, (14, 31, 51, 8), 1342177280], [129, '', 1094, (14, 40, 174, 14), 1350566016], [128, _('Durchsuchen'), 1101, (192, 40, 50, 14), 1342177280], [130, _('Unsichere Nachricht:'), -1, (14, 58, 67, 8), 1342177280], [129, '', 1095, (14, 67, 174, 14), 1350566016], [128, _('Durchsuchen'), 1102, (192, 67, 50, 14), 1342177280], [130, _('Spam:'), -1, (14, 85, 21, 8), 1342177280], [129, '', 1096, (14, 94, 174, 14), 1350566016], [128, _('Durchsuchen'), 1103, (192, 94, 50, 14), 1342177280], [130, _('Zeit, um auf weitere Nachrichten zu warten'), -1, (14, 116, 142, 8), 1342177280], ['msctls_trackbar32', '', 1099, (14, 127, 148, 22), 1342242821], [129, '', 1100, (163, 133, 40, 14), 1350566016], [130, _('Sekunden'), -1, (205, 136, 35, 8), 1342177280]], 'IDD_WIZARD_TRAINING_IS_IMPORTANT': [['', (0, 0, 284, 162), 1354760256, 1024, (8, 'Tahoma')], [130, _('SpamBayes kann nicht effektiv arbeiten, wenn es untrainiert ist.'), -1, (11, 8, 263, 11), 1342177280], [128, _('Training...'), 1017, (225, 140, 49, 15), 1342177280], [130, _("SpamBayes besitzt keine vordefinierten Regeln sondern lernt von Ihnen, Spam von 'guten' Nachrichten (Ham) zu unterscheiden. Sie m\xfcssen SpamBayes deshalb Ordner mit guten und schlechten Nachrichten zum Training zur Verf\xfcgung stellen."), -1, (11, 21, 263, 30), 1342177280], [130, _("In diesem Fall stellt SpamBayes anfangs alle Nachrichten in den Ordner 'unsicher'. W\xe4hrend Sie dann mit den Kn\xf6pfen 'Spam' und 'Kein Spam' die Nachrichten zuordnen, lernt SpamBayes den Umgang mit Ihren Nachrichten."), -1, (22, 61, 252, 29), 1342177280], [130, _('Diese Option wird den Assistenten beenden und erkl\xe4ren, wie Sie Ihre Nachrichten vorsortieren k\xf6nnen. Danach k\xf6nnen Sie SpamBayes trainieren und SpamBayes wird sofort beginnen, effektiv zu arbeiten.'), -1, (22, 106, 252, 27), 1342177280], [130, _("F\xfcr mehr Informationen bet\xe4tigen Sie bitte den Knopf 'Training...'"), -1, (11, 143, 211, 12), 1342177280], [128, _('Ich m\xf6chte ohne Training fortfahren'), 1088, (11, 50, 263, 11), 1342308361], [128, _('Ich werde die Nachrichten vorsortieren und SpamBayes danach konfigurieren.'), 1089, (11, 92, 263, 11), 1342177289]], 'IDD_FOLDER_SELECTOR': [[_('Ordner ausw\xe4hlen'), (0, 0, 247, 215), -1865940800, None, (8, 'Tahoma')], [130, _('&Folders:'), -1, (7, 7, 47, 9), 1342177280], ['SysTreeView32', '', 1040, (7, 21, 172, 140), 1350631735], [128, _('(sub)'), 1041, (7, 167, 126, 9), 1342242819], [130, _('(status1)'), 1043, (7, 180, 220, 9), 1342177280], [130, _('(status2)'), 1044, (7, 194, 220, 9), 1342177280], [128, _('OK'), 1, (190, 21, 50, 14), 1342177281], [128, _('Abbrechen'), 2, (190, 39, 50, 14), 1342177280], [128, _('Alle l\xf6schen'), 1042, (190, 58, 50, 14), 1342177280], [128, _('Neuer Ordner'), 1046, (190, 77, 50, 14), 1342177280]], 'IDD_WIZARD_FOLDERS_WATCH': [['', (0, 0, 284, 162), 1354760256, 1024, (8, 'Tahoma')], [128, _('Durchsuchen'), 1039, (225, 134, 50, 14), 1342177280], [130, _('Ordner, in denen neue Nachrichten eintreffen'), -1, (20, 4, 247, 14), 1342177280], [130, _('SpamBayes muss wissen, in welchen Ordnern neue Nachrichten eintreffen. In den meisen F\xe4llen ist dies der Posteingang. Sie k\xf6nnen aber weitere Ordner angeben, die von SpamBayes \xfcberwacht werden sollen.'), -1, (20, 21, 247, 25), 1342177280], [130, _("Die folgende Liste enth\xe4lt die zu beobachtenden Ordner. Dr\xfccken Sie auf 'Durchsuchen', um die Liste zu \xe4ndern, bzw. auf 'Weiter', um fortzufahren."), -1, (20, 79, 247, 20), 1342177280], [130, _('Wenn Sie den Outlook Regelassistenten benutzen, um Nachrichten zu verschieben, k\xf6nnen Sie solche Ordner zus\xe4tzlich angeben.'), -1, (20, 51, 241, 20), 1342177280], [129, '', 1038, (20, 100, 195, 48), 1350568068]], 'IDD_WIZARD_FINISHED_TRAINED': [['', (0, 0, 284, 162), 1354760256, 1024, (8, 'Tahoma')], [130, _('Gratulation'), -1, (20, 4, 247, 14), 1342177280], [130, _('SpamBayes wurde erfolgreich trainiert und konfiguriert. SpamBayes sollte jetzt bereit sein, die Nachrichten effektiv zu filtern.'), 1035, (20, 35, 247, 26), 1342177280], [130, _("Obwohl SpamBayes jetzt erfolgreich trainiert wurde, lernt SpamBayes weiter. Bitte schauen Sie regelm\xe4\xdfig in den Ordner mit den 'unsicheren' Nachrichten und benutzen die Schaltfl\xe4chen 'Spam' und 'Kein Spam'."), -1, (20, 68, 249, 30), 1342177280], [130, _('Klicken Sie auf Beenden, um den Assistenten zu schlie\xdfen.'), -1, (20, 104, 257, 23), 1342177280]], 'IDD_STATISTICS': [[_('Statistik'), (0, 0, 248, 257), 1354760256, None, (8, 'Tahoma')], [128, _('Statistik'), -1, (7, 3, 241, 229), 1342177287], [130, _('some stats\\nand some more\\nline 3\\nline 4\\nline 5'), 1095, (12, 12, 230, 204), 1342177280], [128, _('Statistik zur\xfccksetzen'), 1096, (165, 238, 83, 14), 1342177280], [130, _('Zuletzt zur\xfcckgesetzt:'), -1, (7, 241, 72, 8), 1342177280], [130, _('<<>>'), 1097, (84, 241, 70, 8), 1342177280]], 'IDD_WIZARD_FOLDERS_TRAIN': [['', (0, 0, 284, 162), 1354760256, 1024, (8, 'Tahoma')], [128, _('Druchsuchen'), 1004, (208, 49, 60, 15), 1342177280], [130, _('Training'), -1, (20, 4, 247, 10), 1342177280], [130, _("Bitte w\xe4hlen Sie die Nachrichten mit dem vorsortierten Spam und den vorsortierten 'guten' Nachrichten."), -1, (20, 16, 243, 16), 1342177280], [129, '', 1083, (20, 49, 179, 14), 1350568064], [130, _('Beispiele von Spam und anderer unerw\xfcnschter Nachrichten finden sich hier:'), -1, (20, 71, 248, 8), 1342177280], [129, '', 1027, (20, 81, 177, 14), 1350568064], [130, _("Beispiele 'guter' Nachrichten finden sich unter"), -1, (20, 38, 153, 8), 1342177280], [128, _('Durchsuchen'), 1005, (208, 81, 60, 15), 1342177280], [130, _('Wenn Sie keine vorsortierten Nachrichten haben oder bereits vorhandene SpamBayes-Daten weiter benutzen m\xf6chten, gehen Sie bitte zur\xfcck und geben an, dass Sie sich nicht vorbereitet haben.'), -1, (20, 128, 243, 26), 1342177280], [128, _('Nachrichten nach dem Training bewerten'), 1008, (20, 108, 163, 16), 1342242819]], 'IDD_ADVANCED': [[_('Erweitert'), (0, 0, 248, 257), 1354760256, 1024, (8, 'Tahoma')], [128, _('Zeitliches Verhalten'), -1, (7, 3, 234, 117), 1342177287], ['msctls_trackbar32', '', 1056, (16, 36, 148, 22), 1342242821], [130, _('Wartezeit vor dem start'), -1, (16, 26, 101, 8), 1342177280], [129, '', 1057, (165, 39, 40, 14), 1350566016], [130, _('seconds'), -1, (208, 41, 28, 8), 1342177280], ['msctls_trackbar32', '', 1058, (16, 73, 148, 22), 1342242821], [130, _('Wartezeit zwischen zwei Elementen'), -1, (16, 62, 142, 8), 1342177280], [129, '', 1059, (165, 79, 40, 14), 1350566016], [130, _('seconds'), -1, (207, 82, 28, 8), 1342177280], [128, _('Nur f\xfcr Ordner, die neue Nachrichten erhalten'), 1060, (16, 100, 217, 10), 1342242819], [128, _('Datenordner zeigen'), 1071, (7, 238, 70, 14), 1342177280], [128, _('Filtern im Hintergrund aktivieren'), 1091, (16, 12, 162, 10), 1342242819], [128, _('Diagnose...'), 1080, (171, 238, 70, 14), 1342177280]]} ids = {'IDC_DELAY1_SLIDER': 1056, 'IDC_PROGRESS': 1000, 'IDD_MANAGER': 101, 'IDD_DIAGNOSTIC': 113, 'IDD_TRAINING': 102, 'IDC_DELAY2_TEXT': 1059, 'IDC_DELAY1_TEXT': 1057, 'IDD_WIZARD': 114, 'IDC_BROWSE_SPAM_SOUND': 1103, 'IDC_STATIC_HAM': 1002, 'IDC_PROGRESS_TEXT': 1001, 'IDD_GENERAL': 108, 'IDC_BROWSE_UNSURE_SOUND': 1102, 'IDC_TAB': 1068, 'IDC_FOLDER_UNSURE': 1033, 'IDC_VERBOSE_LOG': 1061, 'IDC_EDIT1': 1094, 'IDC_BROWSE': 1037, 'IDC_BACK_BTN': 1069, 'IDD_WIZARD_FINISHED_UNCONFIGURED': 119, 'IDC_ACTION_CERTAIN': 1025, 'IDC_BUT_ACT_ALL': 1019, 'IDD_FILTER_NOW': 104, 'IDC_BROWSE_HAM_SOUND': 1101, 'IDC_MARK_SPAM_AS_READ': 1047, 'IDC_RECOVER_RS': 1075, 'IDC_STATIC': -1, 'IDC_PAGE_PLACEHOLDER': 1078, 'IDC_BROWSE_WATCH': 1039, 'IDC_ACCUMULATE_DELAY_TEXT': 1100, 'IDC_FOLDER_HAM': 1083, 'IDD_WIZARD_FOLDERS_REST': 117, 'IDC_SHOW_DATA_FOLDER': 1071, 'IDC_BUT_ACT_SCORE': 1018, '_APS_NEXT_RESOURCE_VALUE': 129, '_APS_NEXT_SYMED_VALUE': 101, 'IDC_SLIDER_CERTAIN': 1023, 'IDC_BUT_UNREAD': 1020, 'IDC_BUT_ABOUT': 1017, 'IDC_BUT_RESCORE': 1008, 'IDC_BUT_SEARCHSUB': 1041, 'IDC_BUT_TRAIN_FROM_SPAM_FOLDER': 1010, 'IDC_LAST_RESET_DATE': 1097, 'IDD_WIZARD_FOLDERS_TRAIN': 120, 'IDC_BUT_FILTER_ENABLE': 1013, 'IDC_ABOUT_BTN': 1072, 'IDD_WIZARD_FINISHED_TRAINED': 122, 'IDD_FOLDER_SELECTOR': 105, 'IDD_STATISTICS': 107, 'IDC_LIST_FOLDERS': 1040, 'IDB_SBWIZLOGO': 125, 'IDC_BUT_VIEW_LOG': 1093, 'IDC_STATUS2': 1044, 'IDC_STATUS1': 1043, 'IDCANCEL': 2, 'IDC_BROWSE_HAM': 1004, 'IDC_BROWSE_SPAM': 1005, 'IDD_WIZARD_FINISHED_UNTRAINED': 116, 'IDC_MARK_UNSURE_AS_READ': 1051, 'IDC_BROWSE_HAM_SOUND2': 1103, 'IDC_BUT_WIZARD': 1070, 'IDC_VERSION': 1009, 'IDC_FOLDER_NAMES': 1036, 'IDC_BUT_TIMER_ENABLED': 1091, 'IDC_SLIDER_UNSURE': 1029, 'IDC_BUT_NEW': 1046, 'IDC_FOLDER_WATCH': 1038, 'IDC_BUT_UNTRAINED': 1088, 'IDC_STATIC_SPAM': 1003, 'IDC_EDIT_UNSURE': 1030, 'IDC_BUT_CLEARALL': 1042, 'IDC_BUT_UNSEEN': 1021, 'IDD_WIZARD_FOLDERS_WATCH': 118, 'IDC_HAM_SOUND': 1094, 'IDC_EDIT_CERTAIN': 1024, 'IDC_BUT_FILTER_DEFINE': 1016, 'IDC_FORWARD_BTN': 1077, '_APS_NEXT_CONTROL_VALUE': 1102, 'IDC_INBOX_TIMER_ONLY': 1060, 'IDD_ADVANCED': 106, 'IDC_WIZ_GRAPHIC': 1092, 'IDC_DEL_SPAM_RS': 1074, 'IDB_FOLDERS': 127, 'IDC_BUT_PREPARATION': 1081, 'IDC_DELAY2_SLIDER': 1058, 'IDC_ACCUMULATE_DELAY_SLIDER': 1099, 'IDC_SAVE_SPAM_SCORE': 1048, 'IDC_FOLDER_CERTAIN': 1027, 'IDB_SBLOGO': 1062, 'IDC_BROWSE_UNSURE': 1034, 'IDC_STATISTICS': 1095, 'IDC_BUT_RESET_STATS': 1096, 'IDC_BUT_TRAIN_TO_SPAM_FOLDER': 1011, 'IDD_FILTER_SPAM': 110, 'IDC_BUT_RESET': 1073, 'IDD_NOTIFICATIONS': 128, 'IDC_ACTION_UNSURE': 1031, 'IDD_WIZARD_TRAIN': 121, 'IDD_WIZARD_FINISHED_TRAIN_LATER': 124, 'IDC_ACTION_HAM': 1032, 'IDC_BUT_REBUILD': 1007, '_APS_NEXT_COMMAND_VALUE': 40001, 'IDC_ENABLE_SOUNDS': 1098, 'IDC_SPAM_SOUND': 1096, 'IDC_UNSURE_SOUND': 1095, 'IDD_WIZARD_TRAINING_IS_IMPORTANT': 123, 'IDC_TRAINING_STATUS': 1035, 'IDD_WIZARD_WELCOME': 115, 'IDC_BUT_TRAIN': 1089, 'IDC_START': 1006, 'IDD_FILTER': 103, 'IDC_LOGO_GRAPHIC': 1063, 'IDC_FILTER_STATUS': 1014, 'IDOK': 1, 'IDC_BROWSE_CERTAIN': 1028, 'IDC_BUT_SHOW_DIAGNOSTICS': 1080, 'IDC_BUT_TRAIN_NOW': 1012} names = {1024: 'IDC_EDIT_CERTAIN', 1: 'IDOK', 2: 'IDCANCEL', 1027: 'IDC_FOLDER_CERTAIN', 1028: 'IDC_BROWSE_CERTAIN', 1029: 'IDC_SLIDER_UNSURE', 1030: 'IDC_EDIT_UNSURE', 1031: 'IDC_ACTION_UNSURE', 1032: 'IDC_ACTION_HAM', 1033: 'IDC_FOLDER_UNSURE', 1034: 'IDC_BROWSE_UNSURE', 1035: 'IDC_TRAINING_STATUS', 1036: 'IDC_FOLDER_NAMES', 1037: 'IDC_BROWSE', 1038: 'IDC_FOLDER_WATCH', 1039: 'IDC_BROWSE_WATCH', 1040: 'IDC_LIST_FOLDERS', 1041: 'IDC_BUT_SEARCHSUB', 1042: 'IDC_BUT_CLEARALL', 1043: 'IDC_STATUS1', 1044: 'IDC_STATUS2', 1046: 'IDC_BUT_NEW', 1047: 'IDC_MARK_SPAM_AS_READ', 1048: 'IDC_SAVE_SPAM_SCORE', 1051: 'IDC_MARK_UNSURE_AS_READ', 1056: 'IDC_DELAY1_SLIDER', 1057: 'IDC_DELAY1_TEXT', 1058: 'IDC_DELAY2_SLIDER', 1059: 'IDC_DELAY2_TEXT', 1060: 'IDC_INBOX_TIMER_ONLY', 1061: 'IDC_VERBOSE_LOG', 1062: 'IDB_SBLOGO', 1063: 'IDC_LOGO_GRAPHIC', 1068: 'IDC_TAB', 1069: 'IDC_BACK_BTN', 1070: 'IDC_BUT_WIZARD', 1071: 'IDC_SHOW_DATA_FOLDER', 1072: 'IDC_ABOUT_BTN', 1073: 'IDC_BUT_RESET', 1074: 'IDC_DEL_SPAM_RS', 1075: 'IDC_RECOVER_RS', 1077: 'IDC_FORWARD_BTN', 1078: 'IDC_PAGE_PLACEHOLDER', 1080: 'IDC_BUT_SHOW_DIAGNOSTICS', 1081: 'IDC_BUT_PREPARATION', 1083: 'IDC_FOLDER_HAM', 1088: 'IDC_BUT_UNTRAINED', 1089: 'IDC_BUT_TRAIN', 1091: 'IDC_BUT_TIMER_ENABLED', 1025: 'IDC_ACTION_CERTAIN', 1093: 'IDC_BUT_VIEW_LOG', 1094: 'IDC_EDIT1', 1095: 'IDC_STATISTICS', 1096: 'IDC_BUT_RESET_STATS', 1097: 'IDC_LAST_RESET_DATE', 1098: 'IDC_ENABLE_SOUNDS', 1099: 'IDC_ACCUMULATE_DELAY_SLIDER', 1100: 'IDC_ACCUMULATE_DELAY_TEXT', 1101: 'IDC_BROWSE_HAM_SOUND', 1102: 'IDC_BROWSE_UNSURE_SOUND', 1103: 'IDC_BROWSE_HAM_SOUND2', 101: 'IDD_MANAGER', 102: 'IDD_TRAINING', 103: 'IDD_FILTER', 104: 'IDD_FILTER_NOW', 105: 'IDD_FOLDER_SELECTOR', 106: 'IDD_ADVANCED', 107: 'IDD_STATISTICS', 108: 'IDD_GENERAL', 110: 'IDD_FILTER_SPAM', 113: 'IDD_DIAGNOSTIC', 114: 'IDD_WIZARD', 115: 'IDD_WIZARD_WELCOME', 116: 'IDD_WIZARD_FINISHED_UNTRAINED', 117: 'IDD_WIZARD_FOLDERS_REST', 118: 'IDD_WIZARD_FOLDERS_WATCH', 119: 'IDD_WIZARD_FINISHED_UNCONFIGURED', 120: 'IDD_WIZARD_FOLDERS_TRAIN', 121: 'IDD_WIZARD_TRAIN', 122: 'IDD_WIZARD_FINISHED_TRAINED', 123: 'IDD_WIZARD_TRAINING_IS_IMPORTANT', 124: 'IDD_WIZARD_FINISHED_TRAIN_LATER', 125: 'IDB_SBWIZLOGO', 127: 'IDB_FOLDERS', 128: 'IDD_NOTIFICATIONS', 129: '_APS_NEXT_RESOURCE_VALUE', 40001: '_APS_NEXT_COMMAND_VALUE', 1092: 'IDC_WIZ_GRAPHIC', 1000: 'IDC_PROGRESS', 1001: 'IDC_PROGRESS_TEXT', 1002: 'IDC_STATIC_HAM', 1003: 'IDC_STATIC_SPAM', 1004: 'IDC_BROWSE_HAM', 1005: 'IDC_BROWSE_SPAM', 1006: 'IDC_START', 1007: 'IDC_BUT_REBUILD', 1008: 'IDC_BUT_RESCORE', 1009: 'IDC_VERSION', 1010: 'IDC_BUT_TRAIN_FROM_SPAM_FOLDER', 1011: 'IDC_BUT_TRAIN_TO_SPAM_FOLDER', 1012: 'IDC_BUT_TRAIN_NOW', 1013: 'IDC_BUT_FILTER_ENABLE', 1014: 'IDC_FILTER_STATUS', 1016: 'IDC_BUT_FILTER_DEFINE', 1017: 'IDC_BUT_ABOUT', 1018: 'IDC_BUT_ACT_SCORE', 1019: 'IDC_BUT_ACT_ALL', 1020: 'IDC_BUT_UNREAD', 1021: 'IDC_BUT_UNSEEN', -1: 'IDC_STATIC', 1023: 'IDC_SLIDER_CERTAIN'} bitmaps = {'IDB_SBWIZLOGO': 'sbwizlogo.bmp', 'IDB_SBLOGO': 'sblogo.bmp', 'IDB_FOLDERS': 'folders.bmp'} def ParseDialogs(s): return FakeParser() spambayes-1.1a6/spambayes/languages/de/LC_MESSAGES/0000775000076500000240000000000011355064627022001 5ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/de/LC_MESSAGES/__init__.py0000664000076500000240000000000010646440126024072 0ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/es/0000775000076500000240000000000011355064627020233 5ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/es/__init__.py0000775000076500000240000000213211116610741022332 0ustar skipstaff00000000000000"""Design-time __init__.py for resourcepackage This is the scanning version of __init__.py for your resource modules. You replace it with a blank or doc-only init when ready to release. """ import os if os.path.splitext(os.path.basename( __file__ ))[0] == "__init__": try: from resourcepackage import package, defaultgenerators generators = defaultgenerators.generators.copy() ### CUSTOMISATION POINT ## import specialised generators here, such as for wxPython #from resourcepackage import wxgenerators #generators.update( wxgenerators.generators ) except ImportError: pass else: package = package.Package( packageName = __name__, directory = os.path.dirname( os.path.abspath(__file__) ), generators = generators, ) package.scan( ### CUSTOMISATION POINT ## force true -> always re-loads from external files, otherwise ## only reloads if the file is newer than the generated .py file. # force = 1, ) spambayes-1.1a6/spambayes/languages/es/_cvsignore.py0000664000076500000240000000034011147407305022731 0ustar skipstaff00000000000000# -*- coding: ISO-8859-1 -*- """Resource _cvsignore (from file .cvsignore)""" # written by resourcepackage: (1, 0, 0) source = '.cvsignore' package = 'spambayes.languages.es' data = "*.pyc\012*.pyo\012_cvsignore.py" ### end spambayes-1.1a6/spambayes/languages/es/DIALOGS/0000775000076500000240000000000011355064627021315 5ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/es/DIALOGS/__init__.py0000775000076500000240000000000010646440125023410 0ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/es/DIALOGS/dialogs.rc0000775000076500000240000010051610646440125023264 0ustar skipstaff00000000000000//Microsoft Developer Studio generated resource script. // #include "dialogs.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" // spambayes dialog definitions ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // Ingls (Estados Unidos) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Dialog // IDD_ADVANCED DIALOGEX 0, 0, 248, 257 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Advanced" FONT 8, "Tahoma", 400, 0, 0x1 BEGIN GROUPBOX "Filter timer",IDC_STATIC,7,3,234,117 CONTROL "",IDC_DELAY1_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,16,36,148,22 LTEXT "Processing start delay",IDC_STATIC,16,26,101,8 EDITTEXT IDC_DELAY1_TEXT,165,39,40,14,ES_AUTOHSCROLL LTEXT "seconds",IDC_STATIC,208,41,28,8 CONTROL "",IDC_DELAY2_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,16,73,148,22 LTEXT "Delay between processing items",IDC_STATIC,16,62,142,8 EDITTEXT IDC_DELAY2_TEXT,165,79,40,14,ES_AUTOHSCROLL LTEXT "seconds",IDC_STATIC,207,82,28,8 CONTROL "Only for folders that receive new mail", IDC_INBOX_TIMER_ONLY,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,16,100,217,10 PUSHBUTTON "Show Data Folder",IDC_SHOW_DATA_FOLDER,7,238,70,14 CONTROL "Enable background filtering",IDC_BUT_TIMER_ENABLED, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,16,12,162,10 PUSHBUTTON "Diagnostics...",IDC_BUT_SHOW_DIAGNOSTICS,171,238,70,14 END IDD_STATISTICS DIALOG DISCARDABLE 0, 0, 248, 257 STYLE WS_CHILD | WS_CAPTION CAPTION "Statistics" FONT 8, "Tahoma" BEGIN GROUPBOX "Statistics",IDC_STATIC,7,3,241,229 LTEXT "some stats\nand some more\nline 3\nline 4\nline 5", IDC_STATISTICS,12,12,230,204 PUSHBUTTON "Reset Statistics",IDC_BUT_RESET_STATS,178,238,70,14 LTEXT "Last reset:",IDC_STATIC,7,241,36,8 LTEXT "<<>>",IDC_LAST_RESET_DATE,47,241,107,8 END IDD_MANAGER DIALOGEX 0, 0, 275, 308 STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "SpamBayes Manager" FONT 8, "Tahoma" BEGIN DEFPUSHBUTTON "Close",IDOK,216,287,50,14 PUSHBUTTON "Cancel",IDCANCEL,155,287,50,14,NOT WS_VISIBLE CONTROL "",IDC_TAB,"SysTabControl32",0x0,8,7,258,276 PUSHBUTTON "About",IDC_ABOUT_BTN,8,287,50,14 END IDD_FILTER_SPAM DIALOGEX 0, 0, 251, 147 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION | WS_SYSMENU CAPTION "Spam" FONT 8, "Tahoma", 400, 0, 0x1 BEGIN LTEXT "Filter the following folders as messages arrive", IDC_STATIC,8,9,168,11 CONTROL "Folder names...\nLine 2",IDC_FOLDER_WATCH,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,7,20,177,12 PUSHBUTTON "Browse...",IDC_BROWSE_WATCH,194,19,50,14 GROUPBOX "Certain Spam",IDC_STATIC,7,43,237,80 LTEXT "To be considered certain spam, a message must score at least", IDC_STATIC,13,52,212,10 CONTROL "",IDC_SLIDER_CERTAIN,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,13,62,165,22 EDITTEXT IDC_EDIT_CERTAIN,184,63,51,14,ES_AUTOHSCROLL LTEXT "and these messages should be:",IDC_STATIC,13,82,107,10 COMBOBOX IDC_ACTION_CERTAIN,13,93,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "to folder",IDC_STATIC,75,95,31,10 CONTROL "Folder names...",IDC_FOLDER_CERTAIN,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,120,93,59,14 PUSHBUTTON "Browse",IDC_BROWSE_CERTAIN,184,93,50,14 CONTROL "Mark spam as read",IDC_MARK_SPAM_AS_READ,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,13,110,81,10 END IDD_FILTER_UNSURE DIALOGEX 0, 0, 249, 124 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION | WS_SYSMENU CAPTION "Possible Spam" FONT 8, "Tahoma", 400, 0, 0x1 BEGIN LTEXT "To be considered uncertain, a message must score at least", IDC_STATIC,12,11,212,10 CONTROL "",IDC_SLIDER_UNSURE,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,12,18,165,20 EDITTEXT IDC_EDIT_UNSURE,183,24,54,14,ES_AUTOHSCROLL LTEXT "and these messages should be:",IDC_STATIC,12,38,107,10 COMBOBOX IDC_ACTION_UNSURE,12,49,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "to folder",IDC_STATIC,74,52,31,10 CONTROL "(folder name)",IDC_FOLDER_UNSURE,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,119,49,59,14 PUSHBUTTON "&Browse",IDC_BROWSE_UNSURE,183,49,50,14 CONTROL "Mark possible spam as read",IDC_MARK_UNSURE_AS_READ, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,70,101,10 END IDD_DIAGNOSTIC DIALOGEX 0, 0, 183, 98 STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Diagnostics" FONT 8, "Tahoma", 400, 0, 0x1 BEGIN LTEXT "These advanced options are for diagnostic or debugging purposes only. You should only change these options if specifically asked to, or you know exactly what they mean.", IDC_STATIC,5,3,174,36 LTEXT "Log file verbosity",IDC_STATIC,5,44,56,8 EDITTEXT IDC_VERBOSE_LOG,73,42,40,14,ES_AUTOHSCROLL PUSHBUTTON "View log...",IDC_BUT_VIEW_LOG,129,41,50,14 CONTROL "Save Spam Score",IDC_SAVE_SPAM_SCORE,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,5,63,72,10 PUSHBUTTON "Cancel",IDCANCEL,69,79,50,14,NOT WS_VISIBLE DEFPUSHBUTTON "Close",IDOK,129,79,50,14 END IDD_WIZARD DIALOGEX 0, 0, 384, 190 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "SpamBayes Configuration Wizard" FONT 8, "Tahoma", 400, 0, 0x1 BEGIN PUSHBUTTON "Cancel",IDCANCEL,328,173,50,14 PUSHBUTTON "<< Back",IDC_BACK_BTN,216,173,50,14 DEFPUSHBUTTON "Next>>,Finish",IDC_FORWARD_BTN,269,173,50,14 CONTROL "",IDC_PAGE_PLACEHOLDER,"Static",SS_ETCHEDFRAME,75,4,303, 167 CONTROL 125,IDC_WIZ_GRAPHIC,"Static",SS_BITMAP,0,0,69,190 END IDD_WIZARD_WELCOME DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Welcome to the SpamBayes configuration wizard", IDC_STATIC,20,4,191,14 LTEXT "This wizard will help you configure the SpamBayes Outlook addin. Please indicate how you have prepared for this application.", IDC_STATIC,20,20,255,18 CONTROL "I haven't prepared for SpamBayes at all.", IDC_BUT_PREPARATION,"Button",BS_AUTORADIOBUTTON | BS_TOP | WS_GROUP,20,42,190,11 CONTROL "I have already sorted good messages (ham) and spam messages into folders that are suitable for training purposes.", IDC_STATIC,"Button",BS_AUTORADIOBUTTON | BS_TOP | BS_MULTILINE,20,59,255,18 CONTROL "I would prefer to configure SpamBayes manually.", IDC_STATIC,"Button",BS_AUTORADIOBUTTON | BS_TOP,20,82, 187,12 LTEXT "If you would like more information about training and configuring SpamBayes, click the About button.", IDC_STATIC,20,103,185,20 PUSHBUTTON "About...",IDC_BUT_ABOUT,215,104,60,15 LTEXT "If you cancel the wizard, you can access it again via the SpamBayes Manager, available from the SpamBayes toolbar.", IDC_STATIC,20,137,232,17 END IDD_WIZARD_FINISHED_UNTRAINED DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Congratulations",IDC_STATIC,20,4,247,14 LTEXT "SpamBayes is now configured and ready to start learning about your Spam", IDC_STATIC,20,22,247,16 LTEXT "As SpamBayes has not been trained, all new mail will arrive in your Unsure folder. As each message arrives, you should use the 'Spam' or 'Not Spam' toolbar buttons as appropriate.", IDC_STATIC,20,42,247,27 LTEXT "If you wish to speed up the training process, you can move all the existing Spam from your Inbox to the new Spam folder, then select 'Training' from the SpamBayes manager.", IDC_STATIC,20,83,247,31 LTEXT "As you train, you will find the accuracy of SpamBayes increases.", IDC_STATIC,20,69,247,15 LTEXT "Click Finish to close the wizard.",IDC_STATIC,20,121, 148,9 END IDD_WIZARD_FOLDERS_REST DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN PUSHBUTTON "Browse...",IDC_BROWSE_SPAM,208,85,60,15 LTEXT "Spam and Unsure Folders",IDC_STATIC,20,4,247,14 LTEXT "SpamBayes uses two folders to manage your Spam - a folder where 'certain' spam is stored, and another for unsure messages.", IDC_STATIC,20,20,247,22 LTEXT "If you enter a folder name and it does not exist, it will be automatically created. If you would prefer to select an existing folder, click the Browse button.", IDC_STATIC,20,44,243,24 EDITTEXT IDC_FOLDER_CERTAIN,20,85,179,14,ES_AUTOHSCROLL LTEXT "Unsure messages will be delivered to a folder named", IDC_STATIC,20,105,186,12 EDITTEXT IDC_FOLDER_UNSURE,20,117,177,14,ES_AUTOHSCROLL LTEXT "Spam will be delivered to a folder named",IDC_STATIC,20, 72,137,8 PUSHBUTTON "Browse...",IDC_BROWSE_UNSURE,208,117,60,15 END IDD_WIZARD_FOLDERS_WATCH DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN PUSHBUTTON "Browse...",IDC_BROWSE_WATCH,225,134,50,14 LTEXT "Folders that receive new messages",IDC_STATIC,20,4,247, 14 LTEXT "SpamBayes needs to know what folders are used to receive new messages. In most cases, this will be your Inbox, but you may also specify additional folders to be watched for spam.", IDC_STATIC,20,21,247,25 LTEXT "The following folders will be watched for new messages. Use the Browse button to change the list, or Next if the list of folders is correct.", IDC_STATIC,20,79,247,20 LTEXT "If you use the Outlook rule wizard to move messages into folders, you may like to select these folders in addition to your inbox.", IDC_STATIC,20,51,241,20 EDITTEXT IDC_FOLDER_WATCH,20,100,195,48,ES_MULTILINE | ES_AUTOHSCROLL | ES_READONLY END IDD_WIZARD_FINISHED_UNCONFIGURED DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Configuration cancelled",IDC_STATIC,20,4,247,14 LTEXT "The main SpamBayes options will now be displayed. You must define your folders and enable SpamBayes before it will begin filtering mail.", IDC_STATIC,20,29,247,16 LTEXT "Click Finish to close the wizard.",IDC_STATIC,20,139, 148,9 END IDD_WIZARD_FOLDERS_TRAIN DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN PUSHBUTTON "Browse...",IDC_BROWSE_HAM,208,49,60,15 LTEXT "Training",IDC_STATIC,20,4,247,10 LTEXT "Please select the folders with the pre-sorted good messages and the folders with the pre-sorted spam messages.", IDC_STATIC,20,16,243,16 EDITTEXT IDC_FOLDER_HAM,20,49,179,14,ES_AUTOHSCROLL | ES_READONLY LTEXT "Examples of Spam, or unwanted messages can be found in", IDC_STATIC,20,71,198,8 EDITTEXT IDC_FOLDER_CERTAIN,20,81,177,14,ES_AUTOHSCROLL | ES_READONLY LTEXT "Examples of good messages can be found in",IDC_STATIC, 20,38,153,8 PUSHBUTTON "Browse...",IDC_BROWSE_SPAM,208,81,60,15 LTEXT "If you have not pre-sorted your messages, or already have training information you wish to keep, please select the Back button and indicate you have not prepared for SpamBayes.", IDC_STATIC,20,128,243,26 CONTROL "Score messages when training is complete", IDC_BUT_RESCORE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,20, 108,163,16 END IDD_WIZARD_TRAIN DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Training",-1,20,4,247,14 LTEXT "SpamBayes is training on your good and spam messages.", -1,20,22,247,16 CONTROL "",IDC_PROGRESS,"msctls_progress32",WS_BORDER,20,45,255, 11 LTEXT "(progress text)",IDC_PROGRESS_TEXT,20,61,257,10 END IDD_WIZARD_FINISHED_TRAINED DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Congratulations",IDC_STATIC,20,4,247,14 LTEXT "SpamBayes has been successfully trained and configured. You should find the system is immediately effective at filtering spam.", IDC_TRAINING_STATUS,20,35,247,26 LTEXT "Even though SpamBayes has been trained, it does continue to learn - please ensure you regularly check your Unsure folder, and use the 'Spam' or 'Not Spam' buttons as appropriate.", IDC_STATIC,20,68,249,30 LTEXT "Click Finish to close the wizard.",IDC_STATIC,20,104, 148,9 END IDD_WIZARD_TRAINING_IS_IMPORTANT DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "SpamBayes will not be effective until it is trained.", IDC_STATIC,11,8,191,14 PUSHBUTTON "About Training...",IDC_BUT_ABOUT,209,140,65,15 LTEXT "SpamBayes is a system that learns about good and bad mail based on examples you provide. It comes with no built-in rules, so must have some training information before it will be effective.", IDC_STATIC,11,21,263,30 LTEXT "In this case, SpamBayes will begin by filtering all mail to an 'Unsure' folder. You can then use the 'Spam' and 'Not Spam' buttons to train each message as it arrives. Slowly SpamBayes will learn about your mail.", IDC_STATIC,22,61,252,29 LTEXT "This option will close the wizard, and provide instructions how to sort your mail. You will then be able to configure SpamBayes and have it be immediately effective at filtering your mail", IDC_STATIC,22,106,252,27 LTEXT "For more information, click the About Training button.", IDC_STATIC,11,143,187,12 CONTROL "I want to continue without training, and let SpamBayes learn as it goes", IDC_BUT_UNTRAINED,"Button",BS_AUTORADIOBUTTON | WS_GROUP, 11,50,263,11 CONTROL "I will pre-sort some good and spam messages, and configure SpamBayes later", IDC_BUT_TRAIN,"Button",BS_AUTORADIOBUTTON,11,92,263,11 END IDD_WIZARD_FINISHED_TRAIN_LATER DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Configuration suspended",IDC_STATIC,20,4,247,14 LTEXT "To perform initial training, you should create a folder that contains only examples of good messages, and another that contains only examples of spam.", IDC_STATIC,20,17,247,27 LTEXT "Click Finish to close the wizard.",IDC_STATIC,20,145, 148,9 LTEXT "For examples of good messages, you may like to use your Inbox - however, it is important you remove all spam from this folder before you commence", IDC_STATIC,20,42,247,26 LTEXT "training. If you have too much spam in your Inbox, you may like to create a temporary folder and copy some examples to it.", IDC_STATIC,20,58,247,17 LTEXT "For examples of spam messages, you may like to look through your Deleted Items folder, and your Inbox. However, you will not be able to specify the Deleted Items folder as examples of spam, so you will need to move them to a folder you create.", IDC_STATIC,20,80,247,35 LTEXT "When you are finished, open the SpamBayes Manager via the SpamBayes toolbar, and re-start the Configuration Wizard.", IDC_STATIC,20,121,245,17 END IDD_NOTIFICATIONS DIALOGEX 0, 0, 248, 257 STYLE DS_SETFONT | WS_CHILD | WS_CAPTION CAPTION "Notifications" FONT 8, "Tahoma", 0, 0, 0x0 BEGIN GROUPBOX "New Mail Sounds",IDC_STATIC,7,3,241,229 CONTROL "Enable new mail notification sounds",IDC_ENABLE_SOUNDS, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,14,17,129,10 LTEXT "Good sound:",IDC_STATIC,14,31,42,8 EDITTEXT IDC_HAM_SOUND,14,40,174,14,ES_AUTOHSCROLL PUSHBUTTON "Browse...",IDC_BROWSE_HAM_SOUND,192,40,50,14 LTEXT "Unsure sound:",IDC_STATIC,14,58,48,8 EDITTEXT IDC_UNSURE_SOUND,14,67,174,14,ES_AUTOHSCROLL PUSHBUTTON "Browse...",IDC_BROWSE_UNSURE_SOUND,192,67,50,14 LTEXT "Spam sound:",IDC_STATIC,14,85,42,8 EDITTEXT IDC_SPAM_SOUND,14,94,174,14,ES_AUTOHSCROLL PUSHBUTTON "Browse...",IDC_BROWSE_SPAM_SOUND,192,94,50,14 LTEXT "Time to wait for additional messages:",IDC_STATIC,14, 116,142,8 CONTROL "",IDC_ACCUMULATE_DELAY_SLIDER,"msctls_trackbar32", TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,14,127,148,22 EDITTEXT IDC_ACCUMULATE_DELAY_TEXT,163,133,40,14,ES_AUTOHSCROLL LTEXT "seconds",IDC_STATIC,205,136,28,8 END ///////////////////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO MOVEABLE PURE BEGIN IDD_ADVANCED, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 241 VERTGUIDE, 16 BOTTOMMARGIN, 204 END IDD_MANAGER, DIALOG BEGIN BOTTOMMARGIN, 253 END IDD_FILTER_SPAM, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 244 TOPMARGIN, 7 BOTTOMMARGIN, 140 END IDD_FILTER_UNSURE, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 242 TOPMARGIN, 7 BOTTOMMARGIN, 117 END IDD_DIAGNOSTIC, DIALOG BEGIN LEFTMARGIN, 5 RIGHTMARGIN, 179 BOTTOMMARGIN, 93 END IDD_WIZARD, DIALOG BEGIN RIGHTMARGIN, 378 END IDD_WIZARD_WELCOME, DIALOG BEGIN VERTGUIDE, 20 VERTGUIDE, 275 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_UNTRAINED, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FOLDERS_REST, DIALOG BEGIN VERTGUIDE, 20 VERTGUIDE, 268 BOTTOMMARGIN, 161 HORZGUIDE, 85 HORZGUIDE, 117 END IDD_WIZARD_FOLDERS_WATCH, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_UNCONFIGURED, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FOLDERS_TRAIN, DIALOG BEGIN VERTGUIDE, 20 VERTGUIDE, 268 BOTTOMMARGIN, 161 HORZGUIDE, 49 HORZGUIDE, 81 END IDD_WIZARD_TRAIN, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_TRAINED, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_TRAINING_IS_IMPORTANT, DIALOG BEGIN VERTGUIDE, 11 VERTGUIDE, 22 VERTGUIDE, 274 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_TRAIN_LATER, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Bitmap // IDB_SBLOGO BITMAP MOVEABLE PURE "sblogo.bmp" IDB_SBWIZLOGO BITMAP MOVEABLE PURE "sbwizlogo.bmp" #endif // Ingls (Estados Unidos) resources ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// // Ingls (Australia) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENA) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_AUS #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Dialog // IDD_GENERAL DIALOGEX 0, 0, 253, 257 STYLE DS_MODALFRAME | WS_CHILD | WS_VISIBLE | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "General" FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "SpamBayes Version Here",IDC_VERSION,6,54,242,8 LTEXT "SpamBayes requiere entrenamiento previo para ser efectivo. Cliquee en la solapa 'Entrenamiento' o use el Asistente de Configuracin para entrenar.", IDC_STATIC,6,67,242,17 LTEXT "Estado de la base de datos de entrenamiento:", IDC_STATIC,6,90,222,8 LTEXT "123 spam messages; 456 good messages\r\nLine2\r\nLine3", IDC_TRAINING_STATUS,6,101,242,27,SS_SUNKEN CONTROL "Habilitar SpamBayes",IDC_BUT_FILTER_ENABLE,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,6,173,97,11 LTEXT "Certain spam is moved to Folder1\nPossible spam is moved too", IDC_FILTER_STATUS,6,146,242,19,SS_SUNKEN PUSHBUTTON "Reiniciar la Configuracin...",IDC_BUT_RESET,6,238,106, 15 PUSHBUTTON "Asistente de Configuracin...",IDC_BUT_WIZARD,142,238, 106,15 LTEXT "Estado del filtro:",IDC_STATIC,6,135,222,8 CONTROL 1062,IDC_LOGO_GRAPHIC,"Static",SS_BITMAP | SS_REALSIZEIMAGE,0,2,275,52 END IDD_TRAINING DIALOGEX 0, 0, 252, 257 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Training" FONT 8, "Tahoma", 0, 0, 0x1 BEGIN GROUPBOX "",IDC_STATIC,5,1,243,113 LTEXT "Folders with known good messages.",IDC_STATIC,11,11,131, 11 CONTROL "",IDC_STATIC_HAM,"Static",SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN,11,21,175,12 PUSHBUTTON "&Browse...",IDC_BROWSE_HAM,192,20,50,14 LTEXT "Folders with spam or other junk messages.",IDC_STATIC, 11,36,171,9 CONTROL "Static",IDC_STATIC_SPAM,"Static",SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,11,46,174,12 PUSHBUTTON "Brow&se...",IDC_BROWSE_SPAM,192,46,50,14 CONTROL "Score &messages after training",IDC_BUT_RESCORE,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,11,64,111,10 CONTROL "&Rebuild entire database",IDC_BUT_REBUILD,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,137,64,92,10 CONTROL "Progress1",IDC_PROGRESS,"msctls_progress32",WS_BORDER, 11,76,231,11 PUSHBUTTON "&Start Training",IDC_START,11,91,54,14,BS_NOTIFY LTEXT "training status training status training status training status training status training status training status ", IDC_PROGRESS_TEXT,75,89,149,17 GROUPBOX "Incremental Training",IDC_STATIC,4,117,244,87 CONTROL "Train that a message is good when it is moved from a spam folder back to the Inbox.", IDC_BUT_TRAIN_FROM_SPAM_FOLDER,"Button",BS_AUTOCHECKBOX | BS_MULTILINE | WS_TABSTOP,11,127,204,18 LTEXT "Clicking 'Not Spam' button should",IDC_STATIC,10,148, 115,10 COMBOBOX IDC_RECOVER_RS,127,145,114,54,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP CONTROL "Train that a message is spam when it is moved to the spam folder.", IDC_BUT_TRAIN_TO_SPAM_FOLDER,"Button",BS_AUTOCHECKBOX | BS_MULTILINE | WS_TABSTOP,11,163,204,16 LTEXT "Clicking 'Spam' button should",IDC_STATIC,10,183,104,10 COMBOBOX IDC_DEL_SPAM_RS,127,180,114,54,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP END IDD_FILTER_NOW DIALOGEX 0, 0, 244, 185 STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Filter Now" FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Filter the following folders",IDC_STATIC,8,9,168,11 CONTROL "Folder names...\nLine 2",IDC_FOLDER_NAMES,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN,7,20,172, 12 PUSHBUTTON "Browse...",IDC_BROWSE,187,19,50,14 GROUPBOX "Filter action",IDC_STATIC,7,38,230,40,WS_GROUP CONTROL "Perform all filter actions",IDC_BUT_ACT_ALL,"Button", BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,15,49,126,10 CONTROL "Score messages, but don't perform filter action", IDC_BUT_ACT_SCORE,"Button",BS_AUTORADIOBUTTON,15,62,203, 10 GROUPBOX "Restrict the filter to",IDC_STATIC,7,84,230,35,WS_GROUP CONTROL "Unread mail",IDC_BUT_UNREAD,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,15,94,149,9 CONTROL "Mail never previously spam filtered",IDC_BUT_UNSEEN, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,15,106,149,9 CONTROL "Progress1",IDC_PROGRESS,"msctls_progress32",WS_BORDER,7, 129,230,11 LTEXT "Static",IDC_PROGRESS_TEXT,7,144,227,10 DEFPUSHBUTTON "Start Filtering",IDC_START,7,161,52,14 PUSHBUTTON "Close",IDCANCEL,187,162,50,14 END IDD_FILTER DIALOGEX 0, 0, 249, 257 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Filtering" FONT 8, "Tahoma" BEGIN LTEXT "Filter the following folders as messages arrive", IDC_STATIC,8,4,168,11 CONTROL "Folder names...\nLine 2",IDC_FOLDER_WATCH,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,7,16,177,12 PUSHBUTTON "Browse...",IDC_BROWSE_WATCH,192,14,50,14 GROUPBOX "Certain Spam",IDC_STATIC,7,33,235,80 LTEXT "To be considered certain spam, a message must score at least", IDC_STATIC,13,42,212,10 CONTROL "Slider1",IDC_SLIDER_CERTAIN,"msctls_trackbar32", TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,13,52,165,22 EDITTEXT IDC_EDIT_CERTAIN,184,53,51,14,ES_AUTOHSCROLL LTEXT "and these messages should be:",IDC_STATIC,13,72,107,10 COMBOBOX IDC_ACTION_CERTAIN,12,83,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "to folder",IDC_STATIC,71,85,28,10 CONTROL "Folder names...",IDC_FOLDER_CERTAIN,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,102,83,77,14 PUSHBUTTON "Browse",IDC_BROWSE_CERTAIN,184,83,50,14 GROUPBOX "Possible Spam",IDC_STATIC,6,117,235,81 LTEXT "To be considered uncertain, a message must score at least", IDC_STATIC,12,128,212,10 CONTROL "Slider1",IDC_SLIDER_UNSURE,"msctls_trackbar32", TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,12,135,165,20 EDITTEXT IDC_EDIT_UNSURE,183,141,54,14,ES_AUTOHSCROLL LTEXT "and these messages should be:",IDC_STATIC,12,155,107,10 COMBOBOX IDC_ACTION_UNSURE,12,166,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "to folder",IDC_STATIC,71,169,27,10 CONTROL "(folder name)",IDC_FOLDER_UNSURE,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,102,166,77,14 PUSHBUTTON "&Browse",IDC_BROWSE_UNSURE,184,166,50,14 CONTROL "Mark spam as read",IDC_MARK_SPAM_AS_READ,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,13,100,81,10 CONTROL "Mark possible spam as read",IDC_MARK_UNSURE_AS_READ, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,186,101,10 GROUPBOX "Certain Good",IDC_STATIC,6,203,235,48 LTEXT "These messages should be:",IDC_STATIC,12,215,107,10 COMBOBOX IDC_ACTION_HAM,12,228,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "to folder",IDC_STATIC,71,230,27,10 CONTROL "(folder name)",IDC_FOLDER_HAM,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,102,228,77,14 PUSHBUTTON "&Browse",IDC_BROWSE_HAM,184,228,50,14 END IDD_FOLDER_SELECTOR DIALOGEX 0, 0, 247, 215 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Dialog" FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "&Folders:",IDC_STATIC,7,7,47,9 CONTROL "",IDC_LIST_FOLDERS,"SysTreeView32",TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | TVS_DISABLEDRAGDROP | TVS_SHOWSELALWAYS | TVS_CHECKBOXES | WS_BORDER | WS_TABSTOP,7,21,172,140 CONTROL "(sub)",IDC_BUT_SEARCHSUB,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,7,167,126,9 LTEXT "(status1)",IDC_STATUS1,7,180,220,9 LTEXT "(status2)",IDC_STATUS2,7,194,220,9 DEFPUSHBUTTON "OK",IDOK,190,21,50,14 PUSHBUTTON "Cancel",IDCANCEL,190,39,50,14 PUSHBUTTON "C&lear All",IDC_BUT_CLEARALL,190,58,50,14 PUSHBUTTON "&New folder",IDC_BUT_NEW,190,77,50,14 END ///////////////////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO MOVEABLE PURE BEGIN IDD_GENERAL, DIALOG BEGIN RIGHTMARGIN, 248 VERTGUIDE, 6 BOTTOMMARGIN, 205 END IDD_TRAINING, DIALOG BEGIN RIGHTMARGIN, 241 VERTGUIDE, 11 VERTGUIDE, 242 BOTTOMMARGIN, 207 END IDD_FILTER_NOW, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 237 TOPMARGIN, 9 BOTTOMMARGIN, 176 END IDD_FILTER, DIALOG BEGIN BOTTOMMARGIN, 254 HORZGUIDE, 127 END END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Bitmap // IDB_FOLDERS BITMAP MOVEABLE PURE "folders.bmp" #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE MOVEABLE PURE BEGIN "dialogs.h\0" END 2 TEXTINCLUDE MOVEABLE PURE BEGIN "#include ""winres.h""\r\n" "// spambayes dialog definitions\r\n" "\0" END 3 TEXTINCLUDE MOVEABLE PURE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED #endif // Ingls (Australia) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED spambayes-1.1a6/spambayes/languages/es/DIALOGS/i18n_dialogs.py0000775000076500000240000006466210646440125024162 0ustar skipstaff00000000000000#c:\spambayes\languages\es\DIALOGS\i18n_dialogs.py #This is a generated file. Please edit c:\spambayes\languages\es\DIALOGS\dialogs.rc instead. _rc_size_=33884 _rc_mtime_=1112074520 try: _ except NameError: def _(s): return s class FakeParser: dialogs = {'IDD_MANAGER': [[_('SpamBayes Manager'), (0, 0, 275, 308), -1865940928, 1024, (8, 'Tahoma')], [128, _('Close'), 1, (216, 287, 50, 14), 1342177281], [128, _('Cancel'), 2, (155, 287, 50, 14), 1073741824], ['SysTabControl32', '', 1068, (8, 7, 258, 276), 1342177280], [128, _('About'), 1072, (8, 287, 50, 14), 1342177280]], 'IDD_DIAGNOSTIC': [[_('Diagnostics'), (0, 0, 183, 98), -1865940928, 1024, (8, 'Tahoma')], [130, _('These advanced options are for diagnostic or debugging purposes only. You should only change these options if specifically asked to, or you know exactly what they mean.'), -1, (5, 3, 174, 36), 1342177280], [130, _('Log file verbosity'), -1, (5, 44, 56, 8), 1342177280], [129, '', 1061, (73, 42, 40, 14), 1350566016], [128, _('View log...'), 1093, (129, 41, 50, 14), 1342177280], [128, _('Save Spam Score'), 1048, (5, 63, 72, 10), 1342242819], [128, _('Cancel'), 2, (69, 79, 50, 14), 1073741824], [128, _('Close'), 1, (129, 79, 50, 14), 1342177281]], 'IDD_FILTER_SPAM': [[_('Spam'), (0, 0, 251, 147), 1355284672, None, (8, 'Tahoma')], [130, _('Filter the following folders as messages arrive'), -1, (8, 9, 168, 11), 1342177280], [130, _('Folder names...\\nLine 2'), 1038, (7, 20, 177, 12), 1342312972], [128, _('Browse...'), 1039, (194, 19, 50, 14), 1342177280], [128, _('Certain Spam'), -1, (7, 43, 237, 80), 1342177287], [130, _('To be considered certain spam, a message must score at least'), -1, (13, 52, 212, 10), 1342177280], ['msctls_trackbar32', '', 1023, (13, 62, 165, 22), 1342242821], [129, '', 1024, (184, 63, 51, 14), 1350566016], [130, _('and these messages should be:'), -1, (13, 82, 107, 10), 1342177280], [133, '', 1025, (13, 93, 55, 40), 1344339971], [130, _('to folder'), -1, (75, 95, 31, 10), 1342177280], [130, _('Folder names...'), 1027, (120, 93, 59, 14), 1342312972], [128, _('Browse'), 1028, (184, 93, 50, 14), 1342177280], [128, _('Mark spam as read'), 1047, (13, 110, 81, 10), 1342242819]], 'IDD_TRAINING': [[_('Training'), (0, 0, 252, 257), 1355284672, 1024, (8, 'Tahoma')], [128, '', -1, (5, 1, 243, 113), 1342177287], [130, _('Folders with known good messages.'), -1, (11, 11, 131, 11), 1342177280], [130, '', 1002, (11, 21, 175, 12), 1342181900], [128, _('&Browse...'), 1004, (192, 20, 50, 14), 1342177280], [130, _('Folders with spam or other junk messages.'), -1, (11, 36, 171, 9), 1342177280], [130, _('Static'), 1003, (11, 46, 174, 12), 1342312972], [128, _('Brow&se...'), 1005, (192, 46, 50, 14), 1342177280], [128, _('Score &messages after training'), 1008, (11, 64, 111, 10), 1342242819], [128, _('&Rebuild entire database'), 1007, (137, 64, 92, 10), 1342242819], ['msctls_progress32', _('Progress1'), 1000, (11, 76, 231, 11), 1350565888], [128, _('&Start Training'), 1006, (11, 91, 54, 14), 1342193664], [130, _('training status training status training status training status training status training status training status '), 1001, (75, 89, 149, 17), 1342177280], [128, _('Incremental Training'), -1, (4, 117, 244, 87), 1342177287], [128, _('Train that a message is good when it is moved from a spam folder back to the Inbox.'), 1010, (11, 127, 204, 18), 1342251011], [130, _("Clicking 'Not Spam' button should"), -1, (10, 148, 115, 10), 1342177280], [133, '', 1075, (127, 145, 114, 54), 1344339971], [128, _('Train that a message is spam when it is moved to the spam folder.'), 1011, (11, 163, 204, 16), 1342251011], [130, _("Clicking 'Spam' button should"), -1, (10, 183, 104, 10), 1342177280], [133, '', 1074, (127, 180, 114, 54), 1344339971]], 'IDD_WIZARD': [[_('SpamBayes Configuration Wizard'), (0, 0, 384, 190), -1865940800, 1024, (8, 'Tahoma')], [128, _('Cancel'), 2, (328, 173, 50, 14), 1342177280], [128, _('<< Back'), 1069, (216, 173, 50, 14), 1342177280], [128, _('Next>>,Finish'), 1077, (269, 173, 50, 14), 1342177281], [130, '', 1078, (75, 4, 303, 167), 1342177298], [130, '125', 1092, (0, 0, 69, 190), 1342177294]], 'IDD_WIZARD_FOLDERS_WATCH': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [128, _('Browse...'), 1039, (225, 134, 50, 14), 1342177280], [130, _('Folders that receive new messages'), -1, (20, 4, 247, 14), 1342177280], [130, _('SpamBayes needs to know what folders are used to receive new messages. In most cases, this will be your Inbox, but you may also specify additional folders to be watched for spam.'), -1, (20, 21, 247, 25), 1342177280], [130, _('The following folders will be watched for new messages. Use the Browse button to change the list, or Next if the list of folders is correct.'), -1, (20, 79, 247, 20), 1342177280], [130, _('If you use the Outlook rule wizard to move messages into folders, you may like to select these folders in addition to your inbox.'), -1, (20, 51, 241, 20), 1342177280], [129, '', 1038, (20, 100, 195, 48), 1350568068]], 'IDD_WIZARD_FINISHED_TRAINED': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('Congratulations'), -1, (20, 4, 247, 14), 1342177280], [130, _('SpamBayes has been successfully trained and configured. You should find the system is immediately effective at filtering spam.'), 1035, (20, 35, 247, 26), 1342177280], [130, _("Even though SpamBayes has been trained, it does continue to learn - please ensure you regularly check your Unsure folder, and use the 'Spam' or 'Not Spam' buttons as appropriate."), -1, (20, 68, 249, 30), 1342177280], [130, _('Click Finish to close the wizard.'), -1, (20, 104, 148, 9), 1342177280]], 'IDD_WIZARD_FOLDERS_TRAIN': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [128, _('Browse...'), 1004, (208, 49, 60, 15), 1342177280], [130, _('Training'), -1, (20, 4, 247, 10), 1342177280], [130, _('Please select the folders with the pre-sorted good messages and the folders with the pre-sorted spam messages.'), -1, (20, 16, 243, 16), 1342177280], [129, '', 1083, (20, 49, 179, 14), 1350568064], [130, _('Examples of Spam, or unwanted messages can be found in'), -1, (20, 71, 198, 8), 1342177280], [129, '', 1027, (20, 81, 177, 14), 1350568064], [130, _('Examples of good messages can be found in'), -1, (20, 38, 153, 8), 1342177280], [128, _('Browse...'), 1005, (208, 81, 60, 15), 1342177280], [130, _('If you have not pre-sorted your messages, or already have training information you wish to keep, please select the Back button and indicate you have not prepared for SpamBayes.'), -1, (20, 128, 243, 26), 1342177280], [128, _('Score messages when training is complete'), 1008, (20, 108, 163, 16), 1342242819]], 'IDD_WIZARD_TRAIN': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('Training'), -1, (20, 4, 247, 14), 1342177280], [130, _('SpamBayes is training on your good and spam messages.'), -1, (20, 22, 247, 16), 1342177280], ['msctls_progress32', '', 1000, (20, 45, 255, 11), 1350565888], [130, _('(progress text)'), 1001, (20, 61, 257, 10), 1342177280]], 'IDD_WIZARD_FINISHED_TRAIN_LATER': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('Configuration suspended'), -1, (20, 4, 247, 14), 1342177280], [130, _('To perform initial training, you should create a folder that contains only examples of good messages, and another that contains only examples of spam.'), -1, (20, 17, 247, 27), 1342177280], [130, _('Click Finish to close the wizard.'), -1, (20, 145, 148, 9), 1342177280], [130, _('For examples of good messages, you may like to use your Inbox - however, it is important you remove all spam from this folder before you commence'), -1, (20, 42, 247, 26), 1342177280], [130, _('training. If you have too much spam in your Inbox, you may like to create a temporary folder and copy some examples to it.'), -1, (20, 58, 247, 17), 1342177280], [130, _('For examples of spam messages, you may like to look through your Deleted Items folder, and your Inbox. However, you will not be able to specify the Deleted Items folder as examples of spam, so you will need to move them to a folder you create.'), -1, (20, 80, 247, 35), 1342177280], [130, _('When you are finished, open the SpamBayes Manager via the SpamBayes toolbar, and re-start the Configuration Wizard.'), -1, (20, 121, 245, 17), 1342177280]], 'IDD_FOLDER_SELECTOR': [[_('Dialog'), (0, 0, 247, 215), -1865940800, None, (8, 'Tahoma')], [130, _('&Folders:'), -1, (7, 7, 47, 9), 1342177280], ['SysTreeView32', '', 1040, (7, 21, 172, 140), 1350631735], [128, _('(sub)'), 1041, (7, 167, 126, 9), 1342242819], [130, _('(status1)'), 1043, (7, 180, 220, 9), 1342177280], [130, _('(status2)'), 1044, (7, 194, 220, 9), 1342177280], [128, _('OK'), 1, (190, 21, 50, 14), 1342177281], [128, _('Cancel'), 2, (190, 39, 50, 14), 1342177280], [128, _('C&lear All'), 1042, (190, 58, 50, 14), 1342177280], [128, _('&New folder'), 1046, (190, 77, 50, 14), 1342177280]], 'IDD_STATISTICS': [[_('Statistics'), (0, 0, 248, 257), 1354760256, None, (8, 'Tahoma')], [128, _('Statistics'), -1, (7, 3, 241, 229), 1342177287], [130, _('some stats\\nand some more\\nline 3\\nline 4\\nline 5'), 1095, (12, 12, 230, 204), 1342177280], [128, _('Reset Statistics'), 1096, (178, 238, 70, 14), 1342177280], [130, _('Last reset:'), -1, (7, 241, 36, 8), 1342177280], [130, _('<<>>'), 1097, (47, 241, 107, 8), 1342177280]], 'IDD_ADVANCED': [[_('Advanced'), (0, 0, 248, 257), 1355284672, 1024, (8, 'Tahoma')], [128, _('Filter timer'), -1, (7, 3, 234, 117), 1342177287], ['msctls_trackbar32', '', 1056, (16, 36, 148, 22), 1342242821], [130, _('Processing start delay'), -1, (16, 26, 101, 8), 1342177280], [129, '', 1057, (165, 39, 40, 14), 1350566016], [130, _('seconds'), -1, (208, 41, 28, 8), 1342177280], ['msctls_trackbar32', '', 1058, (16, 73, 148, 22), 1342242821], [130, _('Delay between processing items'), -1, (16, 62, 142, 8), 1342177280], [129, '', 1059, (165, 79, 40, 14), 1350566016], [130, _('seconds'), -1, (207, 82, 28, 8), 1342177280], [128, _('Only for folders that receive new mail'), 1060, (16, 100, 217, 10), 1342242819], [128, _('Show Data Folder'), 1071, (7, 238, 70, 14), 1342177280], [128, _('Enable background filtering'), 1091, (16, 12, 162, 10), 1342242819], [128, _('Diagnostics...'), 1080, (171, 238, 70, 14), 1342177280]], 'IDD_WIZARD_FINISHED_UNCONFIGURED': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('Configuration cancelled'), -1, (20, 4, 247, 14), 1342177280], [130, _('The main SpamBayes options will now be displayed. You must define your folders and enable SpamBayes before it will begin filtering mail.'), -1, (20, 29, 247, 16), 1342177280], [130, _('Click Finish to close the wizard.'), -1, (20, 139, 148, 9), 1342177280]], 'IDD_WIZARD_WELCOME': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('Welcome to the SpamBayes configuration wizard'), -1, (20, 4, 191, 14), 1342177280], [130, _('This wizard will help you configure the SpamBayes Outlook addin. Please indicate how you have prepared for this application.'), -1, (20, 20, 255, 18), 1342177280], [128, _("I haven't prepared for SpamBayes at all."), 1081, (20, 42, 190, 11), 1342309385], [128, _('I have already sorted good messages (ham) and spam messages into folders that are suitable for training purposes.'), -1, (20, 59, 255, 18), 1342186505], [128, _('I would prefer to configure SpamBayes manually.'), -1, (20, 82, 187, 12), 1342178313], [130, _('If you would like more information about training and configuring SpamBayes, click the About button.'), -1, (20, 103, 185, 20), 1342177280], [128, _('About...'), 1017, (215, 104, 60, 15), 1342177280], [130, _('If you cancel the wizard, you can access it again via the SpamBayes Manager, available from the SpamBayes toolbar.'), -1, (20, 137, 232, 17), 1342177280]], 'IDD_FILTER_NOW': [[_('Filter Now'), (0, 0, 244, 185), -1865940928, 1024, (8, 'Tahoma')], [130, _('Filter the following folders'), -1, (8, 9, 168, 11), 1342177280], [130, _('Folder names...\\nLine 2'), 1036, (7, 20, 172, 12), 1342181900], [128, _('Browse...'), 1037, (187, 19, 50, 14), 1342177280], [128, _('Filter action'), -1, (7, 38, 230, 40), 1342308359], [128, _('Perform all filter actions'), 1019, (15, 49, 126, 10), 1342373897], [128, _("Score messages, but don't perform filter action"), 1018, (15, 62, 203, 10), 1342177289], [128, _('Restrict the filter to'), -1, (7, 84, 230, 35), 1342308359], [128, _('Unread mail'), 1020, (15, 94, 149, 9), 1342242819], [128, _('Mail never previously spam filtered'), 1021, (15, 106, 149, 9), 1342242819], ['msctls_progress32', _('Progress1'), 1000, (7, 129, 230, 11), 1350565888], [130, _('Static'), 1001, (7, 144, 227, 10), 1342177280], [128, _('Start Filtering'), 1006, (7, 161, 52, 14), 1342177281], [128, _('Close'), 2, (187, 162, 50, 14), 1342177280]], 'IDD_WIZARD_TRAINING_IS_IMPORTANT': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('SpamBayes will not be effective until it is trained.'), -1, (11, 8, 191, 14), 1342177280], [128, _('About Training...'), 1017, (209, 140, 65, 15), 1342177280], [130, _('SpamBayes is a system that learns about good and bad mail based on examples you provide. It comes with no built-in rules, so must have some training information before it will be effective.'), -1, (11, 21, 263, 30), 1342177280], [130, _("In this case, SpamBayes will begin by filtering all mail to an 'Unsure' folder. You can then use the 'Spam' and 'Not Spam' buttons to train each message as it arrives. Slowly SpamBayes will learn about your mail."), -1, (22, 61, 252, 29), 1342177280], [130, _('This option will close the wizard, and provide instructions how to sort your mail. You will then be able to configure SpamBayes and have it be immediately effective at filtering your mail'), -1, (22, 106, 252, 27), 1342177280], [130, _('For more information, click the About Training button.'), -1, (11, 143, 187, 12), 1342177280], [128, _('I want to continue without training, and let SpamBayes learn as it goes'), 1088, (11, 50, 263, 11), 1342308361], [128, _('I will pre-sort some good and spam messages, and configure SpamBayes later'), 1089, (11, 92, 263, 11), 1342177289]], 'IDD_FILTER_UNSURE': [[_('Possible Spam'), (0, 0, 249, 124), 1355284672, None, (8, 'Tahoma')], [130, _('To be considered uncertain, a message must score at least'), -1, (12, 11, 212, 10), 1342177280], ['msctls_trackbar32', '', 1029, (12, 18, 165, 20), 1342242821], [129, '', 1030, (183, 24, 54, 14), 1350566016], [130, _('and these messages should be:'), -1, (12, 38, 107, 10), 1342177280], [133, '', 1031, (12, 49, 55, 40), 1344339971], [130, _('to folder'), -1, (74, 52, 31, 10), 1342177280], [130, _('(folder name)'), 1033, (119, 49, 59, 14), 1342312972], [128, _('&Browse'), 1034, (183, 49, 50, 14), 1342177280], [128, _('Mark possible spam as read'), 1051, (12, 70, 101, 10), 1342242819]], 'IDD_WIZARD_FINISHED_UNTRAINED': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('Congratulations'), -1, (20, 4, 247, 14), 1342177280], [130, _('SpamBayes is now configured and ready to start learning about your Spam'), -1, (20, 22, 247, 16), 1342177280], [130, _("As SpamBayes has not been trained, all new mail will arrive in your Unsure folder. As each message arrives, you should use the 'Spam' or 'Not Spam' toolbar buttons as appropriate."), -1, (20, 42, 247, 27), 1342177280], [130, _("If you wish to speed up the training process, you can move all the existing Spam from your Inbox to the new Spam folder, then select 'Training' from the SpamBayes manager."), -1, (20, 83, 247, 31), 1342177280], [130, _('As you train, you will find the accuracy of SpamBayes increases.'), -1, (20, 69, 247, 15), 1342177280], [130, _('Click Finish to close the wizard.'), -1, (20, 121, 148, 9), 1342177280]], 'IDD_GENERAL': [[_('General'), (0, 0, 253, 257), 1355284672, 1024, (8, 'Tahoma')], [130, _('SpamBayes Version Here'), 1009, (6, 54, 242, 8), 1342177280], [130, _("SpamBayes requiere entrenamiento previo para ser efectivo. Cliquee en la solapa 'Entrenamiento' o use el Asistente de Configuraci\xf3n para entrenar."), -1, (6, 67, 242, 17), 1342177280], [130, _('Estado de la base de datos de entrenamiento:'), -1, (6, 90, 222, 8), 1342177280], [130, _('123 spam messages; 456 good messages\\r\\nLine2\\r\\nLine3'), 1035, (6, 101, 242, 27), 1342181376], [128, _('Habilitar SpamBayes'), 1013, (6, 173, 97, 11), 1342242819], [130, _('Certain spam is moved to Folder1\\nPossible spam is moved too'), 1014, (6, 146, 242, 19), 1342181376], [128, _('Reiniciar la Configuraci\xf3n...'), 1073, (6, 238, 106, 15), 1342177280], [128, _('Asistente de Configuraci\xf3n...'), 1070, (142, 238, 106, 15), 1342177280], [130, _('Estado del filtro:'), -1, (6, 135, 222, 8), 1342177280], [130, '1062', 1063, (0, 2, 275, 52), 1342179342]], 'IDD_FILTER': [[_('Filtering'), (0, 0, 249, 257), 1355284672, 1024, (8, 'Tahoma')], [130, _('Filter the following folders as messages arrive'), -1, (8, 4, 168, 11), 1342177280], [130, _('Folder names...\\nLine 2'), 1038, (7, 16, 177, 12), 1342312972], [128, _('Browse...'), 1039, (192, 14, 50, 14), 1342177280], [128, _('Certain Spam'), -1, (7, 33, 235, 80), 1342177287], [130, _('To be considered certain spam, a message must score at least'), -1, (13, 42, 212, 10), 1342177280], ['msctls_trackbar32', _('Slider1'), 1023, (13, 52, 165, 22), 1342242821], [129, '', 1024, (184, 53, 51, 14), 1350566016], [130, _('and these messages should be:'), -1, (13, 72, 107, 10), 1342177280], [133, '', 1025, (12, 83, 55, 40), 1344339971], [130, _('to folder'), -1, (71, 85, 28, 10), 1342177280], [130, _('Folder names...'), 1027, (102, 83, 77, 14), 1342312972], [128, _('Browse'), 1028, (184, 83, 50, 14), 1342177280], [128, _('Possible Spam'), -1, (6, 117, 235, 81), 1342177287], [130, _('To be considered uncertain, a message must score at least'), -1, (12, 128, 212, 10), 1342177280], ['msctls_trackbar32', _('Slider1'), 1029, (12, 135, 165, 20), 1342242821], [129, '', 1030, (183, 141, 54, 14), 1350566016], [130, _('and these messages should be:'), -1, (12, 155, 107, 10), 1342177280], [133, '', 1031, (12, 166, 55, 40), 1344339971], [130, _('to folder'), -1, (71, 169, 27, 10), 1342177280], [130, _('(folder name)'), 1033, (102, 166, 77, 14), 1342312972], [128, _('&Browse'), 1034, (184, 166, 50, 14), 1342177280], [128, _('Mark spam as read'), 1047, (13, 100, 81, 10), 1342242819], [128, _('Mark possible spam as read'), 1051, (12, 186, 101, 10), 1342242819], [128, _('Certain Good'), -1, (6, 203, 235, 48), 1342177287], [130, _('These messages should be:'), -1, (12, 215, 107, 10), 1342177280], [133, '', 1032, (12, 228, 55, 40), 1344339971], [130, _('to folder'), -1, (71, 230, 27, 10), 1342177280], [130, _('(folder name)'), 1083, (102, 228, 77, 14), 1342312972], [128, _('&Browse'), 1004, (184, 228, 50, 14), 1342177280]], 'IDD_NOTIFICATIONS': [[_('Notifications'), (0, 0, 248, 257), 1354760256, None, (8, 'Tahoma')], [128, _('New Mail Sounds'), -1, (7, 3, 241, 229), 1342177287], [128, _('Enable new mail notification sounds'), 1098, (14, 17, 129, 10), 1342242819], [130, _('Good sound:'), -1, (14, 31, 42, 8), 1342177280], [129, '', 1094, (14, 40, 174, 14), 1350566016], [128, _('Browse...'), 1101, (192, 40, 50, 14), 1342177280], [130, _('Unsure sound:'), -1, (14, 58, 48, 8), 1342177280], [129, '', 1095, (14, 67, 174, 14), 1350566016], [128, _('Browse...'), 1102, (192, 67, 50, 14), 1342177280], [130, _('Spam sound:'), -1, (14, 85, 42, 8), 1342177280], [129, '', 1096, (14, 94, 174, 14), 1350566016], [128, _('Browse...'), 1103, (192, 94, 50, 14), 1342177280], [130, _('Time to wait for additional messages:'), -1, (14, 116, 142, 8), 1342177280], ['msctls_trackbar32', '', 1099, (14, 127, 148, 22), 1342242821], [129, '', 1100, (163, 133, 40, 14), 1350566016], [130, _('seconds'), -1, (205, 136, 28, 8), 1342177280]], 'IDD_WIZARD_FOLDERS_REST': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [128, _('Browse...'), 1005, (208, 85, 60, 15), 1342177280], [130, _('Spam and Unsure Folders'), -1, (20, 4, 247, 14), 1342177280], [130, _("SpamBayes uses two folders to manage your Spam - a folder where 'certain' spam is stored, and another for unsure messages."), -1, (20, 20, 247, 22), 1342177280], [130, _('If you enter a folder name and it does not exist, it will be automatically created. If you would prefer to select an existing folder, click the Browse button.'), -1, (20, 44, 243, 24), 1342177280], [129, '', 1027, (20, 85, 179, 14), 1350566016], [130, _('Unsure messages will be delivered to a folder named'), -1, (20, 105, 186, 12), 1342177280], [129, '', 1033, (20, 117, 177, 14), 1350566016], [130, _('Spam will be delivered to a folder named'), -1, (20, 72, 137, 8), 1342177280], [128, _('Browse...'), 1034, (208, 117, 60, 15), 1342177280]]} ids = {'IDC_DELAY1_SLIDER': 1056, 'IDC_PROGRESS': 1000, 'IDD_MANAGER': 101, 'IDD_DIAGNOSTIC': 113, 'IDD_TRAINING': 102, 'IDC_DELAY2_TEXT': 1059, 'IDC_DELAY1_TEXT': 1057, 'IDD_WIZARD': 114, 'IDC_BROWSE_SPAM_SOUND': 1103, 'IDC_STATIC_HAM': 1002, 'IDC_PROGRESS_TEXT': 1001, 'IDD_GENERAL': 108, 'IDC_BROWSE_UNSURE_SOUND': 1102, 'IDC_TAB': 1068, 'IDC_FOLDER_UNSURE': 1033, 'IDC_VERBOSE_LOG': 1061, 'IDC_EDIT1': 1094, 'IDC_BROWSE': 1037, 'IDC_BACK_BTN': 1069, 'IDD_WIZARD_FINISHED_UNCONFIGURED': 119, 'IDC_ACTION_CERTAIN': 1025, 'IDC_BUT_ACT_ALL': 1019, 'IDD_FILTER_NOW': 104, 'IDC_BROWSE_HAM_SOUND': 1101, 'IDC_MARK_SPAM_AS_READ': 1047, 'IDC_RECOVER_RS': 1075, 'IDC_STATIC': -1, 'IDC_PAGE_PLACEHOLDER': 1078, 'IDC_BROWSE_WATCH': 1039, 'IDC_ACCUMULATE_DELAY_TEXT': 1100, 'IDC_FOLDER_HAM': 1083, 'IDD_WIZARD_FOLDERS_REST': 117, 'IDC_SHOW_DATA_FOLDER': 1071, 'IDC_BUT_ACT_SCORE': 1018, '_APS_NEXT_RESOURCE_VALUE': 129, '_APS_NEXT_SYMED_VALUE': 101, 'IDC_SLIDER_CERTAIN': 1023, 'IDC_BUT_UNREAD': 1020, 'IDC_BUT_ABOUT': 1017, 'IDC_BUT_RESCORE': 1008, 'IDC_BUT_SEARCHSUB': 1041, 'IDC_BUT_TRAIN_FROM_SPAM_FOLDER': 1010, 'IDC_LAST_RESET_DATE': 1097, 'IDD_WIZARD_FOLDERS_TRAIN': 120, 'IDC_BUT_FILTER_ENABLE': 1013, 'IDC_ABOUT_BTN': 1072, 'IDD_WIZARD_FINISHED_TRAINED': 122, 'IDD_FOLDER_SELECTOR': 105, 'IDD_STATISTICS': 107, 'IDC_LIST_FOLDERS': 1040, 'IDB_SBWIZLOGO': 125, 'IDC_BUT_VIEW_LOG': 1093, 'IDC_STATUS2': 1044, 'IDC_STATUS1': 1043, 'IDCANCEL': 2, 'IDC_BROWSE_HAM': 1004, 'IDC_BROWSE_SPAM': 1005, 'IDD_WIZARD_FINISHED_UNTRAINED': 116, 'IDC_MARK_UNSURE_AS_READ': 1051, 'IDC_BROWSE_HAM_SOUND2': 1103, 'IDC_BUT_WIZARD': 1070, 'IDC_VERSION': 1009, 'IDC_FOLDER_NAMES': 1036, 'IDC_BUT_TIMER_ENABLED': 1091, 'IDC_SLIDER_UNSURE': 1029, 'IDC_BUT_NEW': 1046, 'IDC_FOLDER_WATCH': 1038, 'IDC_BUT_UNTRAINED': 1088, 'IDC_STATIC_SPAM': 1003, 'IDC_EDIT_UNSURE': 1030, 'IDC_BUT_CLEARALL': 1042, 'IDC_BUT_UNSEEN': 1021, 'IDD_WIZARD_FOLDERS_WATCH': 118, 'IDC_HAM_SOUND': 1094, 'IDC_EDIT_CERTAIN': 1024, 'IDC_BUT_FILTER_DEFINE': 1016, 'IDC_FORWARD_BTN': 1077, '_APS_NEXT_CONTROL_VALUE': 1102, 'IDC_INBOX_TIMER_ONLY': 1060, 'IDD_ADVANCED': 106, 'IDC_WIZ_GRAPHIC': 1092, 'IDD_FILTER_UNSURE': 40002, 'IDC_DEL_SPAM_RS': 1074, 'IDB_FOLDERS': 127, 'IDC_BUT_PREPARATION': 1081, 'IDC_DELAY2_SLIDER': 1058, 'IDC_ACCUMULATE_DELAY_SLIDER': 1099, 'IDC_SAVE_SPAM_SCORE': 1048, 'IDC_FOLDER_CERTAIN': 1027, 'IDB_SBLOGO': 1062, 'IDC_BROWSE_UNSURE': 1034, 'IDC_STATISTICS': 1095, 'IDC_BUT_RESET_STATS': 1096, 'IDC_BUT_TRAIN_TO_SPAM_FOLDER': 1011, 'IDD_FILTER_SPAM': 110, 'IDC_BUT_RESET': 1073, 'IDD_NOTIFICATIONS': 128, 'IDC_ACTION_UNSURE': 1031, 'IDD_WIZARD_TRAIN': 121, 'IDD_WIZARD_FINISHED_TRAIN_LATER': 124, 'IDC_ACTION_HAM': 1032, 'IDC_BUT_REBUILD': 1007, '_APS_NEXT_COMMAND_VALUE': 40001, 'IDC_ENABLE_SOUNDS': 1098, 'IDC_SPAM_SOUND': 1096, 'IDC_UNSURE_SOUND': 1095, 'IDD_WIZARD_TRAINING_IS_IMPORTANT': 123, 'IDC_TRAINING_STATUS': 1035, 'IDD_WIZARD_WELCOME': 115, 'IDC_BUT_TRAIN': 1089, 'IDC_START': 1006, 'IDD_FILTER': 103, 'IDC_LOGO_GRAPHIC': 1063, 'IDC_FILTER_STATUS': 1014, 'IDOK': 1, 'IDC_BROWSE_CERTAIN': 1028, 'IDC_BUT_SHOW_DIAGNOSTICS': 1080, 'IDC_BUT_TRAIN_NOW': 1012} names = {1024: 'IDC_EDIT_CERTAIN', 1: 'IDOK', 2: 'IDCANCEL', 1027: 'IDC_FOLDER_CERTAIN', 1028: 'IDC_BROWSE_CERTAIN', 1029: 'IDC_SLIDER_UNSURE', 1030: 'IDC_EDIT_UNSURE', 1031: 'IDC_ACTION_UNSURE', 1032: 'IDC_ACTION_HAM', 1033: 'IDC_FOLDER_UNSURE', 1034: 'IDC_BROWSE_UNSURE', 1035: 'IDC_TRAINING_STATUS', 1036: 'IDC_FOLDER_NAMES', 1037: 'IDC_BROWSE', 1038: 'IDC_FOLDER_WATCH', 1039: 'IDC_BROWSE_WATCH', 1040: 'IDC_LIST_FOLDERS', 1041: 'IDC_BUT_SEARCHSUB', 1042: 'IDC_BUT_CLEARALL', 1043: 'IDC_STATUS1', 1044: 'IDC_STATUS2', 1046: 'IDC_BUT_NEW', 1047: 'IDC_MARK_SPAM_AS_READ', 1048: 'IDC_SAVE_SPAM_SCORE', 1051: 'IDC_MARK_UNSURE_AS_READ', 1056: 'IDC_DELAY1_SLIDER', 1057: 'IDC_DELAY1_TEXT', 1058: 'IDC_DELAY2_SLIDER', 1059: 'IDC_DELAY2_TEXT', 1060: 'IDC_INBOX_TIMER_ONLY', 1061: 'IDC_VERBOSE_LOG', 1062: 'IDB_SBLOGO', 1063: 'IDC_LOGO_GRAPHIC', 1068: 'IDC_TAB', 1069: 'IDC_BACK_BTN', 1070: 'IDC_BUT_WIZARD', 1071: 'IDC_SHOW_DATA_FOLDER', 1072: 'IDC_ABOUT_BTN', 1073: 'IDC_BUT_RESET', 1074: 'IDC_DEL_SPAM_RS', 1075: 'IDC_RECOVER_RS', 1077: 'IDC_FORWARD_BTN', 1078: 'IDC_PAGE_PLACEHOLDER', 1080: 'IDC_BUT_SHOW_DIAGNOSTICS', 1081: 'IDC_BUT_PREPARATION', 1083: 'IDC_FOLDER_HAM', 1088: 'IDC_BUT_UNTRAINED', 1089: 'IDC_BUT_TRAIN', 40002: 'IDD_FILTER_UNSURE', 1091: 'IDC_BUT_TIMER_ENABLED', 1025: 'IDC_ACTION_CERTAIN', 1093: 'IDC_BUT_VIEW_LOG', 1094: 'IDC_EDIT1', 1095: 'IDC_STATISTICS', 1096: 'IDC_BUT_RESET_STATS', 1097: 'IDC_LAST_RESET_DATE', 1098: 'IDC_ENABLE_SOUNDS', 1099: 'IDC_ACCUMULATE_DELAY_SLIDER', 1100: 'IDC_ACCUMULATE_DELAY_TEXT', 1101: 'IDC_BROWSE_HAM_SOUND', 1102: 'IDC_BROWSE_UNSURE_SOUND', 1103: 'IDC_BROWSE_HAM_SOUND2', 101: 'IDD_MANAGER', 102: 'IDD_TRAINING', 103: 'IDD_FILTER', 104: 'IDD_FILTER_NOW', 105: 'IDD_FOLDER_SELECTOR', 106: 'IDD_ADVANCED', 107: 'IDD_STATISTICS', 108: 'IDD_GENERAL', 110: 'IDD_FILTER_SPAM', 113: 'IDD_DIAGNOSTIC', 114: 'IDD_WIZARD', 115: 'IDD_WIZARD_WELCOME', 116: 'IDD_WIZARD_FINISHED_UNTRAINED', 117: 'IDD_WIZARD_FOLDERS_REST', 118: 'IDD_WIZARD_FOLDERS_WATCH', 119: 'IDD_WIZARD_FINISHED_UNCONFIGURED', 120: 'IDD_WIZARD_FOLDERS_TRAIN', 121: 'IDD_WIZARD_TRAIN', 122: 'IDD_WIZARD_FINISHED_TRAINED', 123: 'IDD_WIZARD_TRAINING_IS_IMPORTANT', 124: 'IDD_WIZARD_FINISHED_TRAIN_LATER', 125: 'IDB_SBWIZLOGO', 127: 'IDB_FOLDERS', 128: 'IDD_NOTIFICATIONS', 129: '_APS_NEXT_RESOURCE_VALUE', 40001: '_APS_NEXT_COMMAND_VALUE', 1092: 'IDC_WIZ_GRAPHIC', 1000: 'IDC_PROGRESS', 1001: 'IDC_PROGRESS_TEXT', 1002: 'IDC_STATIC_HAM', 1003: 'IDC_STATIC_SPAM', 1004: 'IDC_BROWSE_HAM', 1005: 'IDC_BROWSE_SPAM', 1006: 'IDC_START', 1007: 'IDC_BUT_REBUILD', 1008: 'IDC_BUT_RESCORE', 1009: 'IDC_VERSION', 1010: 'IDC_BUT_TRAIN_FROM_SPAM_FOLDER', 1011: 'IDC_BUT_TRAIN_TO_SPAM_FOLDER', 1012: 'IDC_BUT_TRAIN_NOW', 1013: 'IDC_BUT_FILTER_ENABLE', 1014: 'IDC_FILTER_STATUS', 1016: 'IDC_BUT_FILTER_DEFINE', 1017: 'IDC_BUT_ABOUT', 1018: 'IDC_BUT_ACT_SCORE', 1019: 'IDC_BUT_ACT_ALL', 1020: 'IDC_BUT_UNREAD', 1021: 'IDC_BUT_UNSEEN', -1: 'IDC_STATIC', 1023: 'IDC_SLIDER_CERTAIN'} bitmaps = {'IDB_SBWIZLOGO': 'sbwizlogo.bmp', 'IDB_SBLOGO': 'sblogo.bmp', 'IDB_FOLDERS': 'folders.bmp'} def ParseDialogs(s): return FakeParser() spambayes-1.1a6/spambayes/languages/es/LC_MESSAGES/0000775000076500000240000000000011355064627022020 5ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/es/LC_MESSAGES/__init__.py0000775000076500000240000000000010646440125024113 0ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/es_AR/0000775000076500000240000000000011355064627020615 5ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/es_AR/__init__.py0000775000076500000240000000213211116610756022722 0ustar skipstaff00000000000000"""Design-time __init__.py for resourcepackage This is the scanning version of __init__.py for your resource modules. You replace it with a blank or doc-only init when ready to release. """ import os if os.path.splitext(os.path.basename( __file__ ))[0] == "__init__": try: from resourcepackage import package, defaultgenerators generators = defaultgenerators.generators.copy() ### CUSTOMISATION POINT ## import specialised generators here, such as for wxPython #from resourcepackage import wxgenerators #generators.update( wxgenerators.generators ) except ImportError: pass else: package = package.Package( packageName = __name__, directory = os.path.dirname( os.path.abspath(__file__) ), generators = generators, ) package.scan( ### CUSTOMISATION POINT ## force true -> always re-loads from external files, otherwise ## only reloads if the file is newer than the generated .py file. # force = 1, ) spambayes-1.1a6/spambayes/languages/es_AR/_cvsignore.py0000664000076500000240000000032611147407305023317 0ustar skipstaff00000000000000# -*- coding: ISO-8859-1 -*- """Resource _cvsignore (from file .cvsignore)""" # written by resourcepackage: (1, 0, 0) source = '.cvsignore' package = 'spambayes.languages.es_AR' data = "*.pyc\012*.pyo\012" ### end spambayes-1.1a6/spambayes/languages/es_AR/DIALOGS/0000775000076500000240000000000011355064627021677 5ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/es_AR/DIALOGS/__init__.py0000775000076500000240000000000010646440126023773 0ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/es_AR/DIALOGS/dialogs.rc0000775000076500000240000010051610646440126023647 0ustar skipstaff00000000000000//Microsoft Developer Studio generated resource script. // #include "dialogs.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" // spambayes dialog definitions ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // Ingls (Estados Unidos) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Dialog // IDD_ADVANCED DIALOGEX 0, 0, 248, 257 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Advanced" FONT 8, "Tahoma", 400, 0, 0x1 BEGIN GROUPBOX "Filter timer",IDC_STATIC,7,3,234,117 CONTROL "",IDC_DELAY1_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,16,36,148,22 LTEXT "Processing start delay",IDC_STATIC,16,26,101,8 EDITTEXT IDC_DELAY1_TEXT,165,39,40,14,ES_AUTOHSCROLL LTEXT "seconds",IDC_STATIC,208,41,28,8 CONTROL "",IDC_DELAY2_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,16,73,148,22 LTEXT "Delay between processing items",IDC_STATIC,16,62,142,8 EDITTEXT IDC_DELAY2_TEXT,165,79,40,14,ES_AUTOHSCROLL LTEXT "seconds",IDC_STATIC,207,82,28,8 CONTROL "Only for folders that receive new mail", IDC_INBOX_TIMER_ONLY,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,16,100,217,10 PUSHBUTTON "Show Data Folder",IDC_SHOW_DATA_FOLDER,7,238,70,14 CONTROL "Enable background filtering",IDC_BUT_TIMER_ENABLED, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,16,12,162,10 PUSHBUTTON "Diagnostics...",IDC_BUT_SHOW_DIAGNOSTICS,171,238,70,14 END IDD_STATISTICS DIALOG DISCARDABLE 0, 0, 248, 257 STYLE WS_CHILD | WS_CAPTION CAPTION "Statistics" FONT 8, "Tahoma" BEGIN GROUPBOX "Statistics",IDC_STATIC,7,3,241,229 LTEXT "some stats\nand some more\nline 3\nline 4\nline 5", IDC_STATISTICS,12,12,230,204 PUSHBUTTON "Reset Statistics",IDC_BUT_RESET_STATS,178,238,70,14 LTEXT "Last reset:",IDC_STATIC,7,241,36,8 LTEXT "<<>>",IDC_LAST_RESET_DATE,47,241,107,8 END IDD_MANAGER DIALOGEX 0, 0, 275, 308 STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "SpamBayes Manager" FONT 8, "Tahoma" BEGIN DEFPUSHBUTTON "Close",IDOK,216,287,50,14 PUSHBUTTON "Cancel",IDCANCEL,155,287,50,14,NOT WS_VISIBLE CONTROL "",IDC_TAB,"SysTabControl32",0x0,8,7,258,276 PUSHBUTTON "About",IDC_ABOUT_BTN,8,287,50,14 END IDD_FILTER_SPAM DIALOGEX 0, 0, 251, 147 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION | WS_SYSMENU CAPTION "Spam" FONT 8, "Tahoma", 400, 0, 0x1 BEGIN LTEXT "Filter the following folders as messages arrive", IDC_STATIC,8,9,168,11 CONTROL "Folder names...\nLine 2",IDC_FOLDER_WATCH,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,7,20,177,12 PUSHBUTTON "Browse...",IDC_BROWSE_WATCH,194,19,50,14 GROUPBOX "Certain Spam",IDC_STATIC,7,43,237,80 LTEXT "To be considered certain spam, a message must score at least", IDC_STATIC,13,52,212,10 CONTROL "",IDC_SLIDER_CERTAIN,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,13,62,165,22 EDITTEXT IDC_EDIT_CERTAIN,184,63,51,14,ES_AUTOHSCROLL LTEXT "and these messages should be:",IDC_STATIC,13,82,107,10 COMBOBOX IDC_ACTION_CERTAIN,13,93,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "to folder",IDC_STATIC,75,95,31,10 CONTROL "Folder names...",IDC_FOLDER_CERTAIN,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,120,93,59,14 PUSHBUTTON "Browse",IDC_BROWSE_CERTAIN,184,93,50,14 CONTROL "Mark spam as read",IDC_MARK_SPAM_AS_READ,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,13,110,81,10 END IDD_FILTER_UNSURE DIALOGEX 0, 0, 249, 124 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION | WS_SYSMENU CAPTION "Possible Spam" FONT 8, "Tahoma", 400, 0, 0x1 BEGIN LTEXT "To be considered uncertain, a message must score at least", IDC_STATIC,12,11,212,10 CONTROL "",IDC_SLIDER_UNSURE,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,12,18,165,20 EDITTEXT IDC_EDIT_UNSURE,183,24,54,14,ES_AUTOHSCROLL LTEXT "and these messages should be:",IDC_STATIC,12,38,107,10 COMBOBOX IDC_ACTION_UNSURE,12,49,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "to folder",IDC_STATIC,74,52,31,10 CONTROL "(folder name)",IDC_FOLDER_UNSURE,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,119,49,59,14 PUSHBUTTON "&Browse",IDC_BROWSE_UNSURE,183,49,50,14 CONTROL "Mark possible spam as read",IDC_MARK_UNSURE_AS_READ, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,70,101,10 END IDD_DIAGNOSTIC DIALOGEX 0, 0, 183, 98 STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Diagnostics" FONT 8, "Tahoma", 400, 0, 0x1 BEGIN LTEXT "These advanced options are for diagnostic or debugging purposes only. You should only change these options if specifically asked to, or you know exactly what they mean.", IDC_STATIC,5,3,174,36 LTEXT "Log file verbosity",IDC_STATIC,5,44,56,8 EDITTEXT IDC_VERBOSE_LOG,73,42,40,14,ES_AUTOHSCROLL PUSHBUTTON "View log...",IDC_BUT_VIEW_LOG,129,41,50,14 CONTROL "Save Spam Score",IDC_SAVE_SPAM_SCORE,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,5,63,72,10 PUSHBUTTON "Cancel",IDCANCEL,69,79,50,14,NOT WS_VISIBLE DEFPUSHBUTTON "Close",IDOK,129,79,50,14 END IDD_WIZARD DIALOGEX 0, 0, 384, 190 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "SpamBayes Configuration Wizard" FONT 8, "Tahoma", 400, 0, 0x1 BEGIN PUSHBUTTON "Cancel",IDCANCEL,328,173,50,14 PUSHBUTTON "<< Back",IDC_BACK_BTN,216,173,50,14 DEFPUSHBUTTON "Next>>,Finish",IDC_FORWARD_BTN,269,173,50,14 CONTROL "",IDC_PAGE_PLACEHOLDER,"Static",SS_ETCHEDFRAME,75,4,303, 167 CONTROL 125,IDC_WIZ_GRAPHIC,"Static",SS_BITMAP,0,0,69,190 END IDD_WIZARD_WELCOME DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Welcome to the SpamBayes configuration wizard", IDC_STATIC,20,4,191,14 LTEXT "This wizard will help you configure the SpamBayes Outlook addin. Please indicate how you have prepared for this application.", IDC_STATIC,20,20,255,18 CONTROL "I haven't prepared for SpamBayes at all.", IDC_BUT_PREPARATION,"Button",BS_AUTORADIOBUTTON | BS_TOP | WS_GROUP,20,42,190,11 CONTROL "I have already sorted good messages (ham) and spam messages into folders that are suitable for training purposes.", IDC_STATIC,"Button",BS_AUTORADIOBUTTON | BS_TOP | BS_MULTILINE,20,59,255,18 CONTROL "I would prefer to configure SpamBayes manually.", IDC_STATIC,"Button",BS_AUTORADIOBUTTON | BS_TOP,20,82, 187,12 LTEXT "If you would like more information about training and configuring SpamBayes, click the About button.", IDC_STATIC,20,103,185,20 PUSHBUTTON "About...",IDC_BUT_ABOUT,215,104,60,15 LTEXT "If you cancel the wizard, you can access it again via the SpamBayes Manager, available from the SpamBayes toolbar.", IDC_STATIC,20,137,232,17 END IDD_WIZARD_FINISHED_UNTRAINED DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Congratulations",IDC_STATIC,20,4,247,14 LTEXT "SpamBayes is now configured and ready to start learning about your Spam", IDC_STATIC,20,22,247,16 LTEXT "As SpamBayes has not been trained, all new mail will arrive in your Unsure folder. As each message arrives, you should use the 'Spam' or 'Not Spam' toolbar buttons as appropriate.", IDC_STATIC,20,42,247,27 LTEXT "If you wish to speed up the training process, you can move all the existing Spam from your Inbox to the new Spam folder, then select 'Training' from the SpamBayes manager.", IDC_STATIC,20,83,247,31 LTEXT "As you train, you will find the accuracy of SpamBayes increases.", IDC_STATIC,20,69,247,15 LTEXT "Click Finish to close the wizard.",IDC_STATIC,20,121, 148,9 END IDD_WIZARD_FOLDERS_REST DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN PUSHBUTTON "Browse...",IDC_BROWSE_SPAM,208,85,60,15 LTEXT "Spam and Unsure Folders",IDC_STATIC,20,4,247,14 LTEXT "SpamBayes uses two folders to manage your Spam - a folder where 'certain' spam is stored, and another for unsure messages.", IDC_STATIC,20,20,247,22 LTEXT "If you enter a folder name and it does not exist, it will be automatically created. If you would prefer to select an existing folder, click the Browse button.", IDC_STATIC,20,44,243,24 EDITTEXT IDC_FOLDER_CERTAIN,20,85,179,14,ES_AUTOHSCROLL LTEXT "Unsure messages will be delivered to a folder named", IDC_STATIC,20,105,186,12 EDITTEXT IDC_FOLDER_UNSURE,20,117,177,14,ES_AUTOHSCROLL LTEXT "Spam will be delivered to a folder named",IDC_STATIC,20, 72,137,8 PUSHBUTTON "Browse...",IDC_BROWSE_UNSURE,208,117,60,15 END IDD_WIZARD_FOLDERS_WATCH DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN PUSHBUTTON "Browse...",IDC_BROWSE_WATCH,225,134,50,14 LTEXT "Folders that receive new messages",IDC_STATIC,20,4,247, 14 LTEXT "SpamBayes needs to know what folders are used to receive new messages. In most cases, this will be your Inbox, but you may also specify additional folders to be watched for spam.", IDC_STATIC,20,21,247,25 LTEXT "The following folders will be watched for new messages. Use the Browse button to change the list, or Next if the list of folders is correct.", IDC_STATIC,20,79,247,20 LTEXT "If you use the Outlook rule wizard to move messages into folders, you may like to select these folders in addition to your inbox.", IDC_STATIC,20,51,241,20 EDITTEXT IDC_FOLDER_WATCH,20,100,195,48,ES_MULTILINE | ES_AUTOHSCROLL | ES_READONLY END IDD_WIZARD_FINISHED_UNCONFIGURED DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Configuration cancelled",IDC_STATIC,20,4,247,14 LTEXT "The main SpamBayes options will now be displayed. You must define your folders and enable SpamBayes before it will begin filtering mail.", IDC_STATIC,20,29,247,16 LTEXT "Click Finish to close the wizard.",IDC_STATIC,20,139, 148,9 END IDD_WIZARD_FOLDERS_TRAIN DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN PUSHBUTTON "Browse...",IDC_BROWSE_HAM,208,49,60,15 LTEXT "Training",IDC_STATIC,20,4,247,10 LTEXT "Please select the folders with the pre-sorted good messages and the folders with the pre-sorted spam messages.", IDC_STATIC,20,16,243,16 EDITTEXT IDC_FOLDER_HAM,20,49,179,14,ES_AUTOHSCROLL | ES_READONLY LTEXT "Examples of Spam, or unwanted messages can be found in", IDC_STATIC,20,71,198,8 EDITTEXT IDC_FOLDER_CERTAIN,20,81,177,14,ES_AUTOHSCROLL | ES_READONLY LTEXT "Examples of good messages can be found in",IDC_STATIC, 20,38,153,8 PUSHBUTTON "Browse...",IDC_BROWSE_SPAM,208,81,60,15 LTEXT "If you have not pre-sorted your messages, or already have training information you wish to keep, please select the Back button and indicate you have not prepared for SpamBayes.", IDC_STATIC,20,128,243,26 CONTROL "Score messages when training is complete", IDC_BUT_RESCORE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,20, 108,163,16 END IDD_WIZARD_TRAIN DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Training",-1,20,4,247,14 LTEXT "SpamBayes is training on your good and spam messages.", -1,20,22,247,16 CONTROL "",IDC_PROGRESS,"msctls_progress32",WS_BORDER,20,45,255, 11 LTEXT "(progress text)",IDC_PROGRESS_TEXT,20,61,257,10 END IDD_WIZARD_FINISHED_TRAINED DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Congratulations",IDC_STATIC,20,4,247,14 LTEXT "SpamBayes has been successfully trained and configured. You should find the system is immediately effective at filtering spam.", IDC_TRAINING_STATUS,20,35,247,26 LTEXT "Even though SpamBayes has been trained, it does continue to learn - please ensure you regularly check your Unsure folder, and use the 'Spam' or 'Not Spam' buttons as appropriate.", IDC_STATIC,20,68,249,30 LTEXT "Click Finish to close the wizard.",IDC_STATIC,20,104, 148,9 END IDD_WIZARD_TRAINING_IS_IMPORTANT DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "SpamBayes will not be effective until it is trained.", IDC_STATIC,11,8,191,14 PUSHBUTTON "About Training...",IDC_BUT_ABOUT,209,140,65,15 LTEXT "SpamBayes is a system that learns about good and bad mail based on examples you provide. It comes with no built-in rules, so must have some training information before it will be effective.", IDC_STATIC,11,21,263,30 LTEXT "In this case, SpamBayes will begin by filtering all mail to an 'Unsure' folder. You can then use the 'Spam' and 'Not Spam' buttons to train each message as it arrives. Slowly SpamBayes will learn about your mail.", IDC_STATIC,22,61,252,29 LTEXT "This option will close the wizard, and provide instructions how to sort your mail. You will then be able to configure SpamBayes and have it be immediately effective at filtering your mail", IDC_STATIC,22,106,252,27 LTEXT "For more information, click the About Training button.", IDC_STATIC,11,143,187,12 CONTROL "I want to continue without training, and let SpamBayes learn as it goes", IDC_BUT_UNTRAINED,"Button",BS_AUTORADIOBUTTON | WS_GROUP, 11,50,263,11 CONTROL "I will pre-sort some good and spam messages, and configure SpamBayes later", IDC_BUT_TRAIN,"Button",BS_AUTORADIOBUTTON,11,92,263,11 END IDD_WIZARD_FINISHED_TRAIN_LATER DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Configuration suspended",IDC_STATIC,20,4,247,14 LTEXT "To perform initial training, you should create a folder that contains only examples of good messages, and another that contains only examples of spam.", IDC_STATIC,20,17,247,27 LTEXT "Click Finish to close the wizard.",IDC_STATIC,20,145, 148,9 LTEXT "For examples of good messages, you may like to use your Inbox - however, it is important you remove all spam from this folder before you commence", IDC_STATIC,20,42,247,26 LTEXT "training. If you have too much spam in your Inbox, you may like to create a temporary folder and copy some examples to it.", IDC_STATIC,20,58,247,17 LTEXT "For examples of spam messages, you may like to look through your Deleted Items folder, and your Inbox. However, you will not be able to specify the Deleted Items folder as examples of spam, so you will need to move them to a folder you create.", IDC_STATIC,20,80,247,35 LTEXT "When you are finished, open the SpamBayes Manager via the SpamBayes toolbar, and re-start the Configuration Wizard.", IDC_STATIC,20,121,245,17 END IDD_NOTIFICATIONS DIALOGEX 0, 0, 248, 257 STYLE DS_SETFONT | WS_CHILD | WS_CAPTION CAPTION "Notifications" FONT 8, "Tahoma", 0, 0, 0x0 BEGIN GROUPBOX "New Mail Sounds",IDC_STATIC,7,3,241,229 CONTROL "Enable new mail notification sounds",IDC_ENABLE_SOUNDS, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,14,17,129,10 LTEXT "Good sound:",IDC_STATIC,14,31,42,8 EDITTEXT IDC_HAM_SOUND,14,40,174,14,ES_AUTOHSCROLL PUSHBUTTON "Browse...",IDC_BROWSE_HAM_SOUND,192,40,50,14 LTEXT "Unsure sound:",IDC_STATIC,14,58,48,8 EDITTEXT IDC_UNSURE_SOUND,14,67,174,14,ES_AUTOHSCROLL PUSHBUTTON "Browse...",IDC_BROWSE_UNSURE_SOUND,192,67,50,14 LTEXT "Spam sound:",IDC_STATIC,14,85,42,8 EDITTEXT IDC_SPAM_SOUND,14,94,174,14,ES_AUTOHSCROLL PUSHBUTTON "Browse...",IDC_BROWSE_SPAM_SOUND,192,94,50,14 LTEXT "Time to wait for additional messages:",IDC_STATIC,14, 116,142,8 CONTROL "",IDC_ACCUMULATE_DELAY_SLIDER,"msctls_trackbar32", TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,14,127,148,22 EDITTEXT IDC_ACCUMULATE_DELAY_TEXT,163,133,40,14,ES_AUTOHSCROLL LTEXT "seconds",IDC_STATIC,205,136,28,8 END ///////////////////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO MOVEABLE PURE BEGIN IDD_ADVANCED, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 241 VERTGUIDE, 16 BOTTOMMARGIN, 204 END IDD_MANAGER, DIALOG BEGIN BOTTOMMARGIN, 253 END IDD_FILTER_SPAM, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 244 TOPMARGIN, 7 BOTTOMMARGIN, 140 END IDD_FILTER_UNSURE, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 242 TOPMARGIN, 7 BOTTOMMARGIN, 117 END IDD_DIAGNOSTIC, DIALOG BEGIN LEFTMARGIN, 5 RIGHTMARGIN, 179 BOTTOMMARGIN, 93 END IDD_WIZARD, DIALOG BEGIN RIGHTMARGIN, 378 END IDD_WIZARD_WELCOME, DIALOG BEGIN VERTGUIDE, 20 VERTGUIDE, 275 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_UNTRAINED, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FOLDERS_REST, DIALOG BEGIN VERTGUIDE, 20 VERTGUIDE, 268 BOTTOMMARGIN, 161 HORZGUIDE, 85 HORZGUIDE, 117 END IDD_WIZARD_FOLDERS_WATCH, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_UNCONFIGURED, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FOLDERS_TRAIN, DIALOG BEGIN VERTGUIDE, 20 VERTGUIDE, 268 BOTTOMMARGIN, 161 HORZGUIDE, 49 HORZGUIDE, 81 END IDD_WIZARD_TRAIN, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_TRAINED, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_TRAINING_IS_IMPORTANT, DIALOG BEGIN VERTGUIDE, 11 VERTGUIDE, 22 VERTGUIDE, 274 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_TRAIN_LATER, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Bitmap // IDB_SBLOGO BITMAP MOVEABLE PURE "sblogo.bmp" IDB_SBWIZLOGO BITMAP MOVEABLE PURE "sbwizlogo.bmp" #endif // Ingls (Estados Unidos) resources ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// // Ingls (Australia) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENA) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_AUS #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Dialog // IDD_GENERAL DIALOGEX 0, 0, 253, 257 STYLE DS_MODALFRAME | WS_CHILD | WS_VISIBLE | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "General" FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "SpamBayes Version Here",IDC_VERSION,6,54,242,8 LTEXT "SpamBayes requiere entrenamiento previo para ser efectivo. Cliquee en la solapa 'Entrenamiento' o use el Asistente de Configuracin para entrenar.", IDC_STATIC,6,67,242,17 LTEXT "Estado de la base de datos de entrenamiento:", IDC_STATIC,6,90,222,8 LTEXT "123 spam messages; 456 good messages\r\nLine2\r\nLine3", IDC_TRAINING_STATUS,6,101,242,27,SS_SUNKEN CONTROL "Habilitar SpamBayes",IDC_BUT_FILTER_ENABLE,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,6,173,97,11 LTEXT "Certain spam is moved to Folder1\nPossible spam is moved too", IDC_FILTER_STATUS,6,146,242,19,SS_SUNKEN PUSHBUTTON "Reiniciar la Configuracin...",IDC_BUT_RESET,6,238,106, 15 PUSHBUTTON "Asistente de Configuracin...",IDC_BUT_WIZARD,142,238, 106,15 LTEXT "Estado del filtro:",IDC_STATIC,6,135,222,8 CONTROL 1062,IDC_LOGO_GRAPHIC,"Static",SS_BITMAP | SS_REALSIZEIMAGE,0,2,275,52 END IDD_TRAINING DIALOGEX 0, 0, 252, 257 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Training" FONT 8, "Tahoma", 0, 0, 0x1 BEGIN GROUPBOX "",IDC_STATIC,5,1,243,113 LTEXT "Folders with known good messages.",IDC_STATIC,11,11,131, 11 CONTROL "",IDC_STATIC_HAM,"Static",SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN,11,21,175,12 PUSHBUTTON "&Browse...",IDC_BROWSE_HAM,192,20,50,14 LTEXT "Folders with spam or other junk messages.",IDC_STATIC, 11,36,171,9 CONTROL "Static",IDC_STATIC_SPAM,"Static",SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,11,46,174,12 PUSHBUTTON "Brow&se...",IDC_BROWSE_SPAM,192,46,50,14 CONTROL "Score &messages after training",IDC_BUT_RESCORE,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,11,64,111,10 CONTROL "&Rebuild entire database",IDC_BUT_REBUILD,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,137,64,92,10 CONTROL "Progress1",IDC_PROGRESS,"msctls_progress32",WS_BORDER, 11,76,231,11 PUSHBUTTON "&Start Training",IDC_START,11,91,54,14,BS_NOTIFY LTEXT "training status training status training status training status training status training status training status ", IDC_PROGRESS_TEXT,75,89,149,17 GROUPBOX "Incremental Training",IDC_STATIC,4,117,244,87 CONTROL "Train that a message is good when it is moved from a spam folder back to the Inbox.", IDC_BUT_TRAIN_FROM_SPAM_FOLDER,"Button",BS_AUTOCHECKBOX | BS_MULTILINE | WS_TABSTOP,11,127,204,18 LTEXT "Clicking 'Not Spam' button should",IDC_STATIC,10,148, 115,10 COMBOBOX IDC_RECOVER_RS,127,145,114,54,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP CONTROL "Train that a message is spam when it is moved to the spam folder.", IDC_BUT_TRAIN_TO_SPAM_FOLDER,"Button",BS_AUTOCHECKBOX | BS_MULTILINE | WS_TABSTOP,11,163,204,16 LTEXT "Clicking 'Spam' button should",IDC_STATIC,10,183,104,10 COMBOBOX IDC_DEL_SPAM_RS,127,180,114,54,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP END IDD_FILTER_NOW DIALOGEX 0, 0, 244, 185 STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Filter Now" FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Filter the following folders",IDC_STATIC,8,9,168,11 CONTROL "Folder names...\nLine 2",IDC_FOLDER_NAMES,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN,7,20,172, 12 PUSHBUTTON "Browse...",IDC_BROWSE,187,19,50,14 GROUPBOX "Filter action",IDC_STATIC,7,38,230,40,WS_GROUP CONTROL "Perform all filter actions",IDC_BUT_ACT_ALL,"Button", BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,15,49,126,10 CONTROL "Score messages, but don't perform filter action", IDC_BUT_ACT_SCORE,"Button",BS_AUTORADIOBUTTON,15,62,203, 10 GROUPBOX "Restrict the filter to",IDC_STATIC,7,84,230,35,WS_GROUP CONTROL "Unread mail",IDC_BUT_UNREAD,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,15,94,149,9 CONTROL "Mail never previously spam filtered",IDC_BUT_UNSEEN, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,15,106,149,9 CONTROL "Progress1",IDC_PROGRESS,"msctls_progress32",WS_BORDER,7, 129,230,11 LTEXT "Static",IDC_PROGRESS_TEXT,7,144,227,10 DEFPUSHBUTTON "Start Filtering",IDC_START,7,161,52,14 PUSHBUTTON "Close",IDCANCEL,187,162,50,14 END IDD_FILTER DIALOGEX 0, 0, 249, 257 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Filtering" FONT 8, "Tahoma" BEGIN LTEXT "Filter the following folders as messages arrive", IDC_STATIC,8,4,168,11 CONTROL "Folder names...\nLine 2",IDC_FOLDER_WATCH,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,7,16,177,12 PUSHBUTTON "Browse...",IDC_BROWSE_WATCH,192,14,50,14 GROUPBOX "Certain Spam",IDC_STATIC,7,33,235,80 LTEXT "To be considered certain spam, a message must score at least", IDC_STATIC,13,42,212,10 CONTROL "Slider1",IDC_SLIDER_CERTAIN,"msctls_trackbar32", TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,13,52,165,22 EDITTEXT IDC_EDIT_CERTAIN,184,53,51,14,ES_AUTOHSCROLL LTEXT "and these messages should be:",IDC_STATIC,13,72,107,10 COMBOBOX IDC_ACTION_CERTAIN,12,83,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "to folder",IDC_STATIC,71,85,28,10 CONTROL "Folder names...",IDC_FOLDER_CERTAIN,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,102,83,77,14 PUSHBUTTON "Browse",IDC_BROWSE_CERTAIN,184,83,50,14 GROUPBOX "Possible Spam",IDC_STATIC,6,117,235,81 LTEXT "To be considered uncertain, a message must score at least", IDC_STATIC,12,128,212,10 CONTROL "Slider1",IDC_SLIDER_UNSURE,"msctls_trackbar32", TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,12,135,165,20 EDITTEXT IDC_EDIT_UNSURE,183,141,54,14,ES_AUTOHSCROLL LTEXT "and these messages should be:",IDC_STATIC,12,155,107,10 COMBOBOX IDC_ACTION_UNSURE,12,166,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "to folder",IDC_STATIC,71,169,27,10 CONTROL "(folder name)",IDC_FOLDER_UNSURE,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,102,166,77,14 PUSHBUTTON "&Browse",IDC_BROWSE_UNSURE,184,166,50,14 CONTROL "Mark spam as read",IDC_MARK_SPAM_AS_READ,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,13,100,81,10 CONTROL "Mark possible spam as read",IDC_MARK_UNSURE_AS_READ, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,186,101,10 GROUPBOX "Certain Good",IDC_STATIC,6,203,235,48 LTEXT "These messages should be:",IDC_STATIC,12,215,107,10 COMBOBOX IDC_ACTION_HAM,12,228,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "to folder",IDC_STATIC,71,230,27,10 CONTROL "(folder name)",IDC_FOLDER_HAM,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,102,228,77,14 PUSHBUTTON "&Browse",IDC_BROWSE_HAM,184,228,50,14 END IDD_FOLDER_SELECTOR DIALOGEX 0, 0, 247, 215 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Dialog" FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "&Folders:",IDC_STATIC,7,7,47,9 CONTROL "",IDC_LIST_FOLDERS,"SysTreeView32",TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | TVS_DISABLEDRAGDROP | TVS_SHOWSELALWAYS | TVS_CHECKBOXES | WS_BORDER | WS_TABSTOP,7,21,172,140 CONTROL "(sub)",IDC_BUT_SEARCHSUB,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,7,167,126,9 LTEXT "(status1)",IDC_STATUS1,7,180,220,9 LTEXT "(status2)",IDC_STATUS2,7,194,220,9 DEFPUSHBUTTON "OK",IDOK,190,21,50,14 PUSHBUTTON "Cancel",IDCANCEL,190,39,50,14 PUSHBUTTON "C&lear All",IDC_BUT_CLEARALL,190,58,50,14 PUSHBUTTON "&New folder",IDC_BUT_NEW,190,77,50,14 END ///////////////////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO MOVEABLE PURE BEGIN IDD_GENERAL, DIALOG BEGIN RIGHTMARGIN, 248 VERTGUIDE, 6 BOTTOMMARGIN, 205 END IDD_TRAINING, DIALOG BEGIN RIGHTMARGIN, 241 VERTGUIDE, 11 VERTGUIDE, 242 BOTTOMMARGIN, 207 END IDD_FILTER_NOW, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 237 TOPMARGIN, 9 BOTTOMMARGIN, 176 END IDD_FILTER, DIALOG BEGIN BOTTOMMARGIN, 254 HORZGUIDE, 127 END END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Bitmap // IDB_FOLDERS BITMAP MOVEABLE PURE "folders.bmp" #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE MOVEABLE PURE BEGIN "dialogs.h\0" END 2 TEXTINCLUDE MOVEABLE PURE BEGIN "#include ""winres.h""\r\n" "// spambayes dialog definitions\r\n" "\0" END 3 TEXTINCLUDE MOVEABLE PURE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED #endif // Ingls (Australia) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED spambayes-1.1a6/spambayes/languages/es_AR/DIALOGS/i18n_dialogs.py0000775000076500000240000006467010646440126024544 0ustar skipstaff00000000000000#c:\spambayes\languages\es_AR\DIALOGS\i18n_dialogs.py #This is a generated file. Please edit c:\spambayes\languages\es_AR\DIALOGS\dialogs.rc instead. _rc_size_=33884 _rc_mtime_=1112074549 try: _ except NameError: def _(s): return s class FakeParser: dialogs = {'IDD_MANAGER': [[_('SpamBayes Manager'), (0, 0, 275, 308), -1865940928, 1024, (8, 'Tahoma')], [128, _('Close'), 1, (216, 287, 50, 14), 1342177281], [128, _('Cancel'), 2, (155, 287, 50, 14), 1073741824], ['SysTabControl32', '', 1068, (8, 7, 258, 276), 1342177280], [128, _('About'), 1072, (8, 287, 50, 14), 1342177280]], 'IDD_DIAGNOSTIC': [[_('Diagnostics'), (0, 0, 183, 98), -1865940928, 1024, (8, 'Tahoma')], [130, _('These advanced options are for diagnostic or debugging purposes only. You should only change these options if specifically asked to, or you know exactly what they mean.'), -1, (5, 3, 174, 36), 1342177280], [130, _('Log file verbosity'), -1, (5, 44, 56, 8), 1342177280], [129, '', 1061, (73, 42, 40, 14), 1350566016], [128, _('View log...'), 1093, (129, 41, 50, 14), 1342177280], [128, _('Save Spam Score'), 1048, (5, 63, 72, 10), 1342242819], [128, _('Cancel'), 2, (69, 79, 50, 14), 1073741824], [128, _('Close'), 1, (129, 79, 50, 14), 1342177281]], 'IDD_FILTER_SPAM': [[_('Spam'), (0, 0, 251, 147), 1355284672, None, (8, 'Tahoma')], [130, _('Filter the following folders as messages arrive'), -1, (8, 9, 168, 11), 1342177280], [130, _('Folder names...\\nLine 2'), 1038, (7, 20, 177, 12), 1342312972], [128, _('Browse...'), 1039, (194, 19, 50, 14), 1342177280], [128, _('Certain Spam'), -1, (7, 43, 237, 80), 1342177287], [130, _('To be considered certain spam, a message must score at least'), -1, (13, 52, 212, 10), 1342177280], ['msctls_trackbar32', '', 1023, (13, 62, 165, 22), 1342242821], [129, '', 1024, (184, 63, 51, 14), 1350566016], [130, _('and these messages should be:'), -1, (13, 82, 107, 10), 1342177280], [133, '', 1025, (13, 93, 55, 40), 1344339971], [130, _('to folder'), -1, (75, 95, 31, 10), 1342177280], [130, _('Folder names...'), 1027, (120, 93, 59, 14), 1342312972], [128, _('Browse'), 1028, (184, 93, 50, 14), 1342177280], [128, _('Mark spam as read'), 1047, (13, 110, 81, 10), 1342242819]], 'IDD_TRAINING': [[_('Training'), (0, 0, 252, 257), 1355284672, 1024, (8, 'Tahoma')], [128, '', -1, (5, 1, 243, 113), 1342177287], [130, _('Folders with known good messages.'), -1, (11, 11, 131, 11), 1342177280], [130, '', 1002, (11, 21, 175, 12), 1342181900], [128, _('&Browse...'), 1004, (192, 20, 50, 14), 1342177280], [130, _('Folders with spam or other junk messages.'), -1, (11, 36, 171, 9), 1342177280], [130, _('Static'), 1003, (11, 46, 174, 12), 1342312972], [128, _('Brow&se...'), 1005, (192, 46, 50, 14), 1342177280], [128, _('Score &messages after training'), 1008, (11, 64, 111, 10), 1342242819], [128, _('&Rebuild entire database'), 1007, (137, 64, 92, 10), 1342242819], ['msctls_progress32', _('Progress1'), 1000, (11, 76, 231, 11), 1350565888], [128, _('&Start Training'), 1006, (11, 91, 54, 14), 1342193664], [130, _('training status training status training status training status training status training status training status '), 1001, (75, 89, 149, 17), 1342177280], [128, _('Incremental Training'), -1, (4, 117, 244, 87), 1342177287], [128, _('Train that a message is good when it is moved from a spam folder back to the Inbox.'), 1010, (11, 127, 204, 18), 1342251011], [130, _("Clicking 'Not Spam' button should"), -1, (10, 148, 115, 10), 1342177280], [133, '', 1075, (127, 145, 114, 54), 1344339971], [128, _('Train that a message is spam when it is moved to the spam folder.'), 1011, (11, 163, 204, 16), 1342251011], [130, _("Clicking 'Spam' button should"), -1, (10, 183, 104, 10), 1342177280], [133, '', 1074, (127, 180, 114, 54), 1344339971]], 'IDD_WIZARD': [[_('SpamBayes Configuration Wizard'), (0, 0, 384, 190), -1865940800, 1024, (8, 'Tahoma')], [128, _('Cancel'), 2, (328, 173, 50, 14), 1342177280], [128, _('<< Back'), 1069, (216, 173, 50, 14), 1342177280], [128, _('Next>>,Finish'), 1077, (269, 173, 50, 14), 1342177281], [130, '', 1078, (75, 4, 303, 167), 1342177298], [130, '125', 1092, (0, 0, 69, 190), 1342177294]], 'IDD_WIZARD_FOLDERS_WATCH': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [128, _('Browse...'), 1039, (225, 134, 50, 14), 1342177280], [130, _('Folders that receive new messages'), -1, (20, 4, 247, 14), 1342177280], [130, _('SpamBayes needs to know what folders are used to receive new messages. In most cases, this will be your Inbox, but you may also specify additional folders to be watched for spam.'), -1, (20, 21, 247, 25), 1342177280], [130, _('The following folders will be watched for new messages. Use the Browse button to change the list, or Next if the list of folders is correct.'), -1, (20, 79, 247, 20), 1342177280], [130, _('If you use the Outlook rule wizard to move messages into folders, you may like to select these folders in addition to your inbox.'), -1, (20, 51, 241, 20), 1342177280], [129, '', 1038, (20, 100, 195, 48), 1350568068]], 'IDD_WIZARD_FINISHED_TRAINED': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('Congratulations'), -1, (20, 4, 247, 14), 1342177280], [130, _('SpamBayes has been successfully trained and configured. You should find the system is immediately effective at filtering spam.'), 1035, (20, 35, 247, 26), 1342177280], [130, _("Even though SpamBayes has been trained, it does continue to learn - please ensure you regularly check your Unsure folder, and use the 'Spam' or 'Not Spam' buttons as appropriate."), -1, (20, 68, 249, 30), 1342177280], [130, _('Click Finish to close the wizard.'), -1, (20, 104, 148, 9), 1342177280]], 'IDD_WIZARD_FOLDERS_TRAIN': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [128, _('Browse...'), 1004, (208, 49, 60, 15), 1342177280], [130, _('Training'), -1, (20, 4, 247, 10), 1342177280], [130, _('Please select the folders with the pre-sorted good messages and the folders with the pre-sorted spam messages.'), -1, (20, 16, 243, 16), 1342177280], [129, '', 1083, (20, 49, 179, 14), 1350568064], [130, _('Examples of Spam, or unwanted messages can be found in'), -1, (20, 71, 198, 8), 1342177280], [129, '', 1027, (20, 81, 177, 14), 1350568064], [130, _('Examples of good messages can be found in'), -1, (20, 38, 153, 8), 1342177280], [128, _('Browse...'), 1005, (208, 81, 60, 15), 1342177280], [130, _('If you have not pre-sorted your messages, or already have training information you wish to keep, please select the Back button and indicate you have not prepared for SpamBayes.'), -1, (20, 128, 243, 26), 1342177280], [128, _('Score messages when training is complete'), 1008, (20, 108, 163, 16), 1342242819]], 'IDD_WIZARD_TRAIN': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('Training'), -1, (20, 4, 247, 14), 1342177280], [130, _('SpamBayes is training on your good and spam messages.'), -1, (20, 22, 247, 16), 1342177280], ['msctls_progress32', '', 1000, (20, 45, 255, 11), 1350565888], [130, _('(progress text)'), 1001, (20, 61, 257, 10), 1342177280]], 'IDD_WIZARD_FINISHED_TRAIN_LATER': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('Configuration suspended'), -1, (20, 4, 247, 14), 1342177280], [130, _('To perform initial training, you should create a folder that contains only examples of good messages, and another that contains only examples of spam.'), -1, (20, 17, 247, 27), 1342177280], [130, _('Click Finish to close the wizard.'), -1, (20, 145, 148, 9), 1342177280], [130, _('For examples of good messages, you may like to use your Inbox - however, it is important you remove all spam from this folder before you commence'), -1, (20, 42, 247, 26), 1342177280], [130, _('training. If you have too much spam in your Inbox, you may like to create a temporary folder and copy some examples to it.'), -1, (20, 58, 247, 17), 1342177280], [130, _('For examples of spam messages, you may like to look through your Deleted Items folder, and your Inbox. However, you will not be able to specify the Deleted Items folder as examples of spam, so you will need to move them to a folder you create.'), -1, (20, 80, 247, 35), 1342177280], [130, _('When you are finished, open the SpamBayes Manager via the SpamBayes toolbar, and re-start the Configuration Wizard.'), -1, (20, 121, 245, 17), 1342177280]], 'IDD_FOLDER_SELECTOR': [[_('Dialog'), (0, 0, 247, 215), -1865940800, None, (8, 'Tahoma')], [130, _('&Folders:'), -1, (7, 7, 47, 9), 1342177280], ['SysTreeView32', '', 1040, (7, 21, 172, 140), 1350631735], [128, _('(sub)'), 1041, (7, 167, 126, 9), 1342242819], [130, _('(status1)'), 1043, (7, 180, 220, 9), 1342177280], [130, _('(status2)'), 1044, (7, 194, 220, 9), 1342177280], [128, _('OK'), 1, (190, 21, 50, 14), 1342177281], [128, _('Cancel'), 2, (190, 39, 50, 14), 1342177280], [128, _('C&lear All'), 1042, (190, 58, 50, 14), 1342177280], [128, _('&New folder'), 1046, (190, 77, 50, 14), 1342177280]], 'IDD_STATISTICS': [[_('Statistics'), (0, 0, 248, 257), 1354760256, None, (8, 'Tahoma')], [128, _('Statistics'), -1, (7, 3, 241, 229), 1342177287], [130, _('some stats\\nand some more\\nline 3\\nline 4\\nline 5'), 1095, (12, 12, 230, 204), 1342177280], [128, _('Reset Statistics'), 1096, (178, 238, 70, 14), 1342177280], [130, _('Last reset:'), -1, (7, 241, 36, 8), 1342177280], [130, _('<<>>'), 1097, (47, 241, 107, 8), 1342177280]], 'IDD_ADVANCED': [[_('Advanced'), (0, 0, 248, 257), 1355284672, 1024, (8, 'Tahoma')], [128, _('Filter timer'), -1, (7, 3, 234, 117), 1342177287], ['msctls_trackbar32', '', 1056, (16, 36, 148, 22), 1342242821], [130, _('Processing start delay'), -1, (16, 26, 101, 8), 1342177280], [129, '', 1057, (165, 39, 40, 14), 1350566016], [130, _('seconds'), -1, (208, 41, 28, 8), 1342177280], ['msctls_trackbar32', '', 1058, (16, 73, 148, 22), 1342242821], [130, _('Delay between processing items'), -1, (16, 62, 142, 8), 1342177280], [129, '', 1059, (165, 79, 40, 14), 1350566016], [130, _('seconds'), -1, (207, 82, 28, 8), 1342177280], [128, _('Only for folders that receive new mail'), 1060, (16, 100, 217, 10), 1342242819], [128, _('Show Data Folder'), 1071, (7, 238, 70, 14), 1342177280], [128, _('Enable background filtering'), 1091, (16, 12, 162, 10), 1342242819], [128, _('Diagnostics...'), 1080, (171, 238, 70, 14), 1342177280]], 'IDD_WIZARD_FINISHED_UNCONFIGURED': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('Configuration cancelled'), -1, (20, 4, 247, 14), 1342177280], [130, _('The main SpamBayes options will now be displayed. You must define your folders and enable SpamBayes before it will begin filtering mail.'), -1, (20, 29, 247, 16), 1342177280], [130, _('Click Finish to close the wizard.'), -1, (20, 139, 148, 9), 1342177280]], 'IDD_WIZARD_WELCOME': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('Welcome to the SpamBayes configuration wizard'), -1, (20, 4, 191, 14), 1342177280], [130, _('This wizard will help you configure the SpamBayes Outlook addin. Please indicate how you have prepared for this application.'), -1, (20, 20, 255, 18), 1342177280], [128, _("I haven't prepared for SpamBayes at all."), 1081, (20, 42, 190, 11), 1342309385], [128, _('I have already sorted good messages (ham) and spam messages into folders that are suitable for training purposes.'), -1, (20, 59, 255, 18), 1342186505], [128, _('I would prefer to configure SpamBayes manually.'), -1, (20, 82, 187, 12), 1342178313], [130, _('If you would like more information about training and configuring SpamBayes, click the About button.'), -1, (20, 103, 185, 20), 1342177280], [128, _('About...'), 1017, (215, 104, 60, 15), 1342177280], [130, _('If you cancel the wizard, you can access it again via the SpamBayes Manager, available from the SpamBayes toolbar.'), -1, (20, 137, 232, 17), 1342177280]], 'IDD_FILTER_NOW': [[_('Filter Now'), (0, 0, 244, 185), -1865940928, 1024, (8, 'Tahoma')], [130, _('Filter the following folders'), -1, (8, 9, 168, 11), 1342177280], [130, _('Folder names...\\nLine 2'), 1036, (7, 20, 172, 12), 1342181900], [128, _('Browse...'), 1037, (187, 19, 50, 14), 1342177280], [128, _('Filter action'), -1, (7, 38, 230, 40), 1342308359], [128, _('Perform all filter actions'), 1019, (15, 49, 126, 10), 1342373897], [128, _("Score messages, but don't perform filter action"), 1018, (15, 62, 203, 10), 1342177289], [128, _('Restrict the filter to'), -1, (7, 84, 230, 35), 1342308359], [128, _('Unread mail'), 1020, (15, 94, 149, 9), 1342242819], [128, _('Mail never previously spam filtered'), 1021, (15, 106, 149, 9), 1342242819], ['msctls_progress32', _('Progress1'), 1000, (7, 129, 230, 11), 1350565888], [130, _('Static'), 1001, (7, 144, 227, 10), 1342177280], [128, _('Start Filtering'), 1006, (7, 161, 52, 14), 1342177281], [128, _('Close'), 2, (187, 162, 50, 14), 1342177280]], 'IDD_WIZARD_TRAINING_IS_IMPORTANT': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('SpamBayes will not be effective until it is trained.'), -1, (11, 8, 191, 14), 1342177280], [128, _('About Training...'), 1017, (209, 140, 65, 15), 1342177280], [130, _('SpamBayes is a system that learns about good and bad mail based on examples you provide. It comes with no built-in rules, so must have some training information before it will be effective.'), -1, (11, 21, 263, 30), 1342177280], [130, _("In this case, SpamBayes will begin by filtering all mail to an 'Unsure' folder. You can then use the 'Spam' and 'Not Spam' buttons to train each message as it arrives. Slowly SpamBayes will learn about your mail."), -1, (22, 61, 252, 29), 1342177280], [130, _('This option will close the wizard, and provide instructions how to sort your mail. You will then be able to configure SpamBayes and have it be immediately effective at filtering your mail'), -1, (22, 106, 252, 27), 1342177280], [130, _('For more information, click the About Training button.'), -1, (11, 143, 187, 12), 1342177280], [128, _('I want to continue without training, and let SpamBayes learn as it goes'), 1088, (11, 50, 263, 11), 1342308361], [128, _('I will pre-sort some good and spam messages, and configure SpamBayes later'), 1089, (11, 92, 263, 11), 1342177289]], 'IDD_FILTER_UNSURE': [[_('Possible Spam'), (0, 0, 249, 124), 1355284672, None, (8, 'Tahoma')], [130, _('To be considered uncertain, a message must score at least'), -1, (12, 11, 212, 10), 1342177280], ['msctls_trackbar32', '', 1029, (12, 18, 165, 20), 1342242821], [129, '', 1030, (183, 24, 54, 14), 1350566016], [130, _('and these messages should be:'), -1, (12, 38, 107, 10), 1342177280], [133, '', 1031, (12, 49, 55, 40), 1344339971], [130, _('to folder'), -1, (74, 52, 31, 10), 1342177280], [130, _('(folder name)'), 1033, (119, 49, 59, 14), 1342312972], [128, _('&Browse'), 1034, (183, 49, 50, 14), 1342177280], [128, _('Mark possible spam as read'), 1051, (12, 70, 101, 10), 1342242819]], 'IDD_WIZARD_FINISHED_UNTRAINED': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('Congratulations'), -1, (20, 4, 247, 14), 1342177280], [130, _('SpamBayes is now configured and ready to start learning about your Spam'), -1, (20, 22, 247, 16), 1342177280], [130, _("As SpamBayes has not been trained, all new mail will arrive in your Unsure folder. As each message arrives, you should use the 'Spam' or 'Not Spam' toolbar buttons as appropriate."), -1, (20, 42, 247, 27), 1342177280], [130, _("If you wish to speed up the training process, you can move all the existing Spam from your Inbox to the new Spam folder, then select 'Training' from the SpamBayes manager."), -1, (20, 83, 247, 31), 1342177280], [130, _('As you train, you will find the accuracy of SpamBayes increases.'), -1, (20, 69, 247, 15), 1342177280], [130, _('Click Finish to close the wizard.'), -1, (20, 121, 148, 9), 1342177280]], 'IDD_GENERAL': [[_('General'), (0, 0, 253, 257), 1355284672, 1024, (8, 'Tahoma')], [130, _('SpamBayes Version Here'), 1009, (6, 54, 242, 8), 1342177280], [130, _("SpamBayes requiere entrenamiento previo para ser efectivo. Cliquee en la solapa 'Entrenamiento' o use el Asistente de Configuraci\xf3n para entrenar."), -1, (6, 67, 242, 17), 1342177280], [130, _('Estado de la base de datos de entrenamiento:'), -1, (6, 90, 222, 8), 1342177280], [130, _('123 spam messages; 456 good messages\\r\\nLine2\\r\\nLine3'), 1035, (6, 101, 242, 27), 1342181376], [128, _('Habilitar SpamBayes'), 1013, (6, 173, 97, 11), 1342242819], [130, _('Certain spam is moved to Folder1\\nPossible spam is moved too'), 1014, (6, 146, 242, 19), 1342181376], [128, _('Reiniciar la Configuraci\xf3n...'), 1073, (6, 238, 106, 15), 1342177280], [128, _('Asistente de Configuraci\xf3n...'), 1070, (142, 238, 106, 15), 1342177280], [130, _('Estado del filtro:'), -1, (6, 135, 222, 8), 1342177280], [130, '1062', 1063, (0, 2, 275, 52), 1342179342]], 'IDD_FILTER': [[_('Filtering'), (0, 0, 249, 257), 1355284672, 1024, (8, 'Tahoma')], [130, _('Filter the following folders as messages arrive'), -1, (8, 4, 168, 11), 1342177280], [130, _('Folder names...\\nLine 2'), 1038, (7, 16, 177, 12), 1342312972], [128, _('Browse...'), 1039, (192, 14, 50, 14), 1342177280], [128, _('Certain Spam'), -1, (7, 33, 235, 80), 1342177287], [130, _('To be considered certain spam, a message must score at least'), -1, (13, 42, 212, 10), 1342177280], ['msctls_trackbar32', _('Slider1'), 1023, (13, 52, 165, 22), 1342242821], [129, '', 1024, (184, 53, 51, 14), 1350566016], [130, _('and these messages should be:'), -1, (13, 72, 107, 10), 1342177280], [133, '', 1025, (12, 83, 55, 40), 1344339971], [130, _('to folder'), -1, (71, 85, 28, 10), 1342177280], [130, _('Folder names...'), 1027, (102, 83, 77, 14), 1342312972], [128, _('Browse'), 1028, (184, 83, 50, 14), 1342177280], [128, _('Possible Spam'), -1, (6, 117, 235, 81), 1342177287], [130, _('To be considered uncertain, a message must score at least'), -1, (12, 128, 212, 10), 1342177280], ['msctls_trackbar32', _('Slider1'), 1029, (12, 135, 165, 20), 1342242821], [129, '', 1030, (183, 141, 54, 14), 1350566016], [130, _('and these messages should be:'), -1, (12, 155, 107, 10), 1342177280], [133, '', 1031, (12, 166, 55, 40), 1344339971], [130, _('to folder'), -1, (71, 169, 27, 10), 1342177280], [130, _('(folder name)'), 1033, (102, 166, 77, 14), 1342312972], [128, _('&Browse'), 1034, (184, 166, 50, 14), 1342177280], [128, _('Mark spam as read'), 1047, (13, 100, 81, 10), 1342242819], [128, _('Mark possible spam as read'), 1051, (12, 186, 101, 10), 1342242819], [128, _('Certain Good'), -1, (6, 203, 235, 48), 1342177287], [130, _('These messages should be:'), -1, (12, 215, 107, 10), 1342177280], [133, '', 1032, (12, 228, 55, 40), 1344339971], [130, _('to folder'), -1, (71, 230, 27, 10), 1342177280], [130, _('(folder name)'), 1083, (102, 228, 77, 14), 1342312972], [128, _('&Browse'), 1004, (184, 228, 50, 14), 1342177280]], 'IDD_NOTIFICATIONS': [[_('Notifications'), (0, 0, 248, 257), 1354760256, None, (8, 'Tahoma')], [128, _('New Mail Sounds'), -1, (7, 3, 241, 229), 1342177287], [128, _('Enable new mail notification sounds'), 1098, (14, 17, 129, 10), 1342242819], [130, _('Good sound:'), -1, (14, 31, 42, 8), 1342177280], [129, '', 1094, (14, 40, 174, 14), 1350566016], [128, _('Browse...'), 1101, (192, 40, 50, 14), 1342177280], [130, _('Unsure sound:'), -1, (14, 58, 48, 8), 1342177280], [129, '', 1095, (14, 67, 174, 14), 1350566016], [128, _('Browse...'), 1102, (192, 67, 50, 14), 1342177280], [130, _('Spam sound:'), -1, (14, 85, 42, 8), 1342177280], [129, '', 1096, (14, 94, 174, 14), 1350566016], [128, _('Browse...'), 1103, (192, 94, 50, 14), 1342177280], [130, _('Time to wait for additional messages:'), -1, (14, 116, 142, 8), 1342177280], ['msctls_trackbar32', '', 1099, (14, 127, 148, 22), 1342242821], [129, '', 1100, (163, 133, 40, 14), 1350566016], [130, _('seconds'), -1, (205, 136, 28, 8), 1342177280]], 'IDD_WIZARD_FOLDERS_REST': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [128, _('Browse...'), 1005, (208, 85, 60, 15), 1342177280], [130, _('Spam and Unsure Folders'), -1, (20, 4, 247, 14), 1342177280], [130, _("SpamBayes uses two folders to manage your Spam - a folder where 'certain' spam is stored, and another for unsure messages."), -1, (20, 20, 247, 22), 1342177280], [130, _('If you enter a folder name and it does not exist, it will be automatically created. If you would prefer to select an existing folder, click the Browse button.'), -1, (20, 44, 243, 24), 1342177280], [129, '', 1027, (20, 85, 179, 14), 1350566016], [130, _('Unsure messages will be delivered to a folder named'), -1, (20, 105, 186, 12), 1342177280], [129, '', 1033, (20, 117, 177, 14), 1350566016], [130, _('Spam will be delivered to a folder named'), -1, (20, 72, 137, 8), 1342177280], [128, _('Browse...'), 1034, (208, 117, 60, 15), 1342177280]]} ids = {'IDC_DELAY1_SLIDER': 1056, 'IDC_PROGRESS': 1000, 'IDD_MANAGER': 101, 'IDD_DIAGNOSTIC': 113, 'IDD_TRAINING': 102, 'IDC_DELAY2_TEXT': 1059, 'IDC_DELAY1_TEXT': 1057, 'IDD_WIZARD': 114, 'IDC_BROWSE_SPAM_SOUND': 1103, 'IDC_STATIC_HAM': 1002, 'IDC_PROGRESS_TEXT': 1001, 'IDD_GENERAL': 108, 'IDC_BROWSE_UNSURE_SOUND': 1102, 'IDC_TAB': 1068, 'IDC_FOLDER_UNSURE': 1033, 'IDC_VERBOSE_LOG': 1061, 'IDC_EDIT1': 1094, 'IDC_BROWSE': 1037, 'IDC_BACK_BTN': 1069, 'IDD_WIZARD_FINISHED_UNCONFIGURED': 119, 'IDC_ACTION_CERTAIN': 1025, 'IDC_BUT_ACT_ALL': 1019, 'IDD_FILTER_NOW': 104, 'IDC_BROWSE_HAM_SOUND': 1101, 'IDC_MARK_SPAM_AS_READ': 1047, 'IDC_RECOVER_RS': 1075, 'IDC_STATIC': -1, 'IDC_PAGE_PLACEHOLDER': 1078, 'IDC_BROWSE_WATCH': 1039, 'IDC_ACCUMULATE_DELAY_TEXT': 1100, 'IDC_FOLDER_HAM': 1083, 'IDD_WIZARD_FOLDERS_REST': 117, 'IDC_SHOW_DATA_FOLDER': 1071, 'IDC_BUT_ACT_SCORE': 1018, '_APS_NEXT_RESOURCE_VALUE': 129, '_APS_NEXT_SYMED_VALUE': 101, 'IDC_SLIDER_CERTAIN': 1023, 'IDC_BUT_UNREAD': 1020, 'IDC_BUT_ABOUT': 1017, 'IDC_BUT_RESCORE': 1008, 'IDC_BUT_SEARCHSUB': 1041, 'IDC_BUT_TRAIN_FROM_SPAM_FOLDER': 1010, 'IDC_LAST_RESET_DATE': 1097, 'IDD_WIZARD_FOLDERS_TRAIN': 120, 'IDC_BUT_FILTER_ENABLE': 1013, 'IDC_ABOUT_BTN': 1072, 'IDD_WIZARD_FINISHED_TRAINED': 122, 'IDD_FOLDER_SELECTOR': 105, 'IDD_STATISTICS': 107, 'IDC_LIST_FOLDERS': 1040, 'IDB_SBWIZLOGO': 125, 'IDC_BUT_VIEW_LOG': 1093, 'IDC_STATUS2': 1044, 'IDC_STATUS1': 1043, 'IDCANCEL': 2, 'IDC_BROWSE_HAM': 1004, 'IDC_BROWSE_SPAM': 1005, 'IDD_WIZARD_FINISHED_UNTRAINED': 116, 'IDC_MARK_UNSURE_AS_READ': 1051, 'IDC_BROWSE_HAM_SOUND2': 1103, 'IDC_BUT_WIZARD': 1070, 'IDC_VERSION': 1009, 'IDC_FOLDER_NAMES': 1036, 'IDC_BUT_TIMER_ENABLED': 1091, 'IDC_SLIDER_UNSURE': 1029, 'IDC_BUT_NEW': 1046, 'IDC_FOLDER_WATCH': 1038, 'IDC_BUT_UNTRAINED': 1088, 'IDC_STATIC_SPAM': 1003, 'IDC_EDIT_UNSURE': 1030, 'IDC_BUT_CLEARALL': 1042, 'IDC_BUT_UNSEEN': 1021, 'IDD_WIZARD_FOLDERS_WATCH': 118, 'IDC_HAM_SOUND': 1094, 'IDC_EDIT_CERTAIN': 1024, 'IDC_BUT_FILTER_DEFINE': 1016, 'IDC_FORWARD_BTN': 1077, '_APS_NEXT_CONTROL_VALUE': 1102, 'IDC_INBOX_TIMER_ONLY': 1060, 'IDD_ADVANCED': 106, 'IDC_WIZ_GRAPHIC': 1092, 'IDD_FILTER_UNSURE': 40002, 'IDC_DEL_SPAM_RS': 1074, 'IDB_FOLDERS': 127, 'IDC_BUT_PREPARATION': 1081, 'IDC_DELAY2_SLIDER': 1058, 'IDC_ACCUMULATE_DELAY_SLIDER': 1099, 'IDC_SAVE_SPAM_SCORE': 1048, 'IDC_FOLDER_CERTAIN': 1027, 'IDB_SBLOGO': 1062, 'IDC_BROWSE_UNSURE': 1034, 'IDC_STATISTICS': 1095, 'IDC_BUT_RESET_STATS': 1096, 'IDC_BUT_TRAIN_TO_SPAM_FOLDER': 1011, 'IDD_FILTER_SPAM': 110, 'IDC_BUT_RESET': 1073, 'IDD_NOTIFICATIONS': 128, 'IDC_ACTION_UNSURE': 1031, 'IDD_WIZARD_TRAIN': 121, 'IDD_WIZARD_FINISHED_TRAIN_LATER': 124, 'IDC_ACTION_HAM': 1032, 'IDC_BUT_REBUILD': 1007, '_APS_NEXT_COMMAND_VALUE': 40001, 'IDC_ENABLE_SOUNDS': 1098, 'IDC_SPAM_SOUND': 1096, 'IDC_UNSURE_SOUND': 1095, 'IDD_WIZARD_TRAINING_IS_IMPORTANT': 123, 'IDC_TRAINING_STATUS': 1035, 'IDD_WIZARD_WELCOME': 115, 'IDC_BUT_TRAIN': 1089, 'IDC_START': 1006, 'IDD_FILTER': 103, 'IDC_LOGO_GRAPHIC': 1063, 'IDC_FILTER_STATUS': 1014, 'IDOK': 1, 'IDC_BROWSE_CERTAIN': 1028, 'IDC_BUT_SHOW_DIAGNOSTICS': 1080, 'IDC_BUT_TRAIN_NOW': 1012} names = {1024: 'IDC_EDIT_CERTAIN', 1: 'IDOK', 2: 'IDCANCEL', 1027: 'IDC_FOLDER_CERTAIN', 1028: 'IDC_BROWSE_CERTAIN', 1029: 'IDC_SLIDER_UNSURE', 1030: 'IDC_EDIT_UNSURE', 1031: 'IDC_ACTION_UNSURE', 1032: 'IDC_ACTION_HAM', 1033: 'IDC_FOLDER_UNSURE', 1034: 'IDC_BROWSE_UNSURE', 1035: 'IDC_TRAINING_STATUS', 1036: 'IDC_FOLDER_NAMES', 1037: 'IDC_BROWSE', 1038: 'IDC_FOLDER_WATCH', 1039: 'IDC_BROWSE_WATCH', 1040: 'IDC_LIST_FOLDERS', 1041: 'IDC_BUT_SEARCHSUB', 1042: 'IDC_BUT_CLEARALL', 1043: 'IDC_STATUS1', 1044: 'IDC_STATUS2', 1046: 'IDC_BUT_NEW', 1047: 'IDC_MARK_SPAM_AS_READ', 1048: 'IDC_SAVE_SPAM_SCORE', 1051: 'IDC_MARK_UNSURE_AS_READ', 1056: 'IDC_DELAY1_SLIDER', 1057: 'IDC_DELAY1_TEXT', 1058: 'IDC_DELAY2_SLIDER', 1059: 'IDC_DELAY2_TEXT', 1060: 'IDC_INBOX_TIMER_ONLY', 1061: 'IDC_VERBOSE_LOG', 1062: 'IDB_SBLOGO', 1063: 'IDC_LOGO_GRAPHIC', 1068: 'IDC_TAB', 1069: 'IDC_BACK_BTN', 1070: 'IDC_BUT_WIZARD', 1071: 'IDC_SHOW_DATA_FOLDER', 1072: 'IDC_ABOUT_BTN', 1073: 'IDC_BUT_RESET', 1074: 'IDC_DEL_SPAM_RS', 1075: 'IDC_RECOVER_RS', 1077: 'IDC_FORWARD_BTN', 1078: 'IDC_PAGE_PLACEHOLDER', 1080: 'IDC_BUT_SHOW_DIAGNOSTICS', 1081: 'IDC_BUT_PREPARATION', 1083: 'IDC_FOLDER_HAM', 1088: 'IDC_BUT_UNTRAINED', 1089: 'IDC_BUT_TRAIN', 40002: 'IDD_FILTER_UNSURE', 1091: 'IDC_BUT_TIMER_ENABLED', 1025: 'IDC_ACTION_CERTAIN', 1093: 'IDC_BUT_VIEW_LOG', 1094: 'IDC_EDIT1', 1095: 'IDC_STATISTICS', 1096: 'IDC_BUT_RESET_STATS', 1097: 'IDC_LAST_RESET_DATE', 1098: 'IDC_ENABLE_SOUNDS', 1099: 'IDC_ACCUMULATE_DELAY_SLIDER', 1100: 'IDC_ACCUMULATE_DELAY_TEXT', 1101: 'IDC_BROWSE_HAM_SOUND', 1102: 'IDC_BROWSE_UNSURE_SOUND', 1103: 'IDC_BROWSE_HAM_SOUND2', 101: 'IDD_MANAGER', 102: 'IDD_TRAINING', 103: 'IDD_FILTER', 104: 'IDD_FILTER_NOW', 105: 'IDD_FOLDER_SELECTOR', 106: 'IDD_ADVANCED', 107: 'IDD_STATISTICS', 108: 'IDD_GENERAL', 110: 'IDD_FILTER_SPAM', 113: 'IDD_DIAGNOSTIC', 114: 'IDD_WIZARD', 115: 'IDD_WIZARD_WELCOME', 116: 'IDD_WIZARD_FINISHED_UNTRAINED', 117: 'IDD_WIZARD_FOLDERS_REST', 118: 'IDD_WIZARD_FOLDERS_WATCH', 119: 'IDD_WIZARD_FINISHED_UNCONFIGURED', 120: 'IDD_WIZARD_FOLDERS_TRAIN', 121: 'IDD_WIZARD_TRAIN', 122: 'IDD_WIZARD_FINISHED_TRAINED', 123: 'IDD_WIZARD_TRAINING_IS_IMPORTANT', 124: 'IDD_WIZARD_FINISHED_TRAIN_LATER', 125: 'IDB_SBWIZLOGO', 127: 'IDB_FOLDERS', 128: 'IDD_NOTIFICATIONS', 129: '_APS_NEXT_RESOURCE_VALUE', 40001: '_APS_NEXT_COMMAND_VALUE', 1092: 'IDC_WIZ_GRAPHIC', 1000: 'IDC_PROGRESS', 1001: 'IDC_PROGRESS_TEXT', 1002: 'IDC_STATIC_HAM', 1003: 'IDC_STATIC_SPAM', 1004: 'IDC_BROWSE_HAM', 1005: 'IDC_BROWSE_SPAM', 1006: 'IDC_START', 1007: 'IDC_BUT_REBUILD', 1008: 'IDC_BUT_RESCORE', 1009: 'IDC_VERSION', 1010: 'IDC_BUT_TRAIN_FROM_SPAM_FOLDER', 1011: 'IDC_BUT_TRAIN_TO_SPAM_FOLDER', 1012: 'IDC_BUT_TRAIN_NOW', 1013: 'IDC_BUT_FILTER_ENABLE', 1014: 'IDC_FILTER_STATUS', 1016: 'IDC_BUT_FILTER_DEFINE', 1017: 'IDC_BUT_ABOUT', 1018: 'IDC_BUT_ACT_SCORE', 1019: 'IDC_BUT_ACT_ALL', 1020: 'IDC_BUT_UNREAD', 1021: 'IDC_BUT_UNSEEN', -1: 'IDC_STATIC', 1023: 'IDC_SLIDER_CERTAIN'} bitmaps = {'IDB_SBWIZLOGO': 'sbwizlogo.bmp', 'IDB_SBLOGO': 'sblogo.bmp', 'IDB_FOLDERS': 'folders.bmp'} def ParseDialogs(s): return FakeParser() spambayes-1.1a6/spambayes/languages/es_AR/LC_MESSAGES/0000775000076500000240000000000011355064627022402 5ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/es_AR/LC_MESSAGES/__init__.py0000775000076500000240000000000010646440127024477 0ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/fr/0000775000076500000240000000000011355064627020233 5ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/fr/__init__.py0000775000076500000240000000213211116611000022317 0ustar skipstaff00000000000000"""Design-time __init__.py for resourcepackage This is the scanning version of __init__.py for your resource modules. You replace it with a blank or doc-only init when ready to release. """ import os if os.path.splitext(os.path.basename( __file__ ))[0] == "__init__": try: from resourcepackage import package, defaultgenerators generators = defaultgenerators.generators.copy() ### CUSTOMISATION POINT ## import specialised generators here, such as for wxPython #from resourcepackage import wxgenerators #generators.update( wxgenerators.generators ) except ImportError: pass else: package = package.Package( packageName = __name__, directory = os.path.dirname( os.path.abspath(__file__) ), generators = generators, ) package.scan( ### CUSTOMISATION POINT ## force true -> always re-loads from external files, otherwise ## only reloads if the file is newer than the generated .py file. # force = 1, ) spambayes-1.1a6/spambayes/languages/fr/_cvsignore.py0000664000076500000240000000034411147407305022735 0ustar skipstaff00000000000000# -*- coding: ISO-8859-1 -*- """Resource _cvsignore (from file .cvsignore)""" # written by resourcepackage: (1, 0, 0) source = '.cvsignore' package = 'spambayes.languages.fr' data = "*.pyc\012*.pyo\012_cvsignore.py\012" ### end spambayes-1.1a6/spambayes/languages/fr/DIALOGS/0000775000076500000240000000000011355064627021315 5ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/fr/DIALOGS/__init__.py0000775000076500000240000000000010646440125023410 0ustar skipstaff00000000000000spambayes-1.1a6/spambayes/languages/fr/DIALOGS/dialogs.rc0000775000076500000240000010042310646440125023261 0ustar skipstaff00000000000000//Microsoft Developer Studio generated resource script. // #include "dialogs.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" // spambayes dialog definitions ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (U.S.) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Dialog // IDD_ADVANCED DIALOGEX 0, 0, 248, 257 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Avanc" FONT 8, "Tahoma" BEGIN GROUPBOX "Dlais de filtrage",IDC_STATIC,7,3,234,117 CONTROL "",IDC_DELAY1_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,16,36,148,22 LTEXT "Dlai avant filtrage",IDC_STATIC,16,26,101,8 EDITTEXT IDC_DELAY1_TEXT,165,39,40,14,ES_AUTOHSCROLL LTEXT "secondes",IDC_STATIC,208,41,28,8 CONTROL "",IDC_DELAY2_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,16,73,148,22 LTEXT "Dlai de filtrage entre deux messages",IDC_STATIC,16,62, 142,8 EDITTEXT IDC_DELAY2_TEXT,165,79,40,14,ES_AUTOHSCROLL LTEXT "secondes",IDC_STATIC,207,82,28,8 CONTROL "Seulement pour les dossiers qui reoivent de nouveaux messages", IDC_INBOX_TIMER_ONLY,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,16,100,217,10 PUSHBUTTON "Afficher le rpertoire de donnes",IDC_SHOW_DATA_FOLDER, 7,238,111,14 CONTROL "Activer le filtrage en tche de fond", IDC_BUT_TIMER_ENABLED,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,16,12,162,10 PUSHBUTTON "Diagnostiques...",IDC_BUT_SHOW_DIAGNOSTICS,171,238,70, 14 END IDD_STATISTICS DIALOG DISCARDABLE 0, 0, 248, 257 STYLE WS_CHILD | WS_CAPTION CAPTION "Statistiques" FONT 8, "Tahoma" BEGIN GROUPBOX "Statistiques",IDC_STATIC,7,3,241,229 LTEXT "some stats\nand some more\nline 3\nline 4\nline 5", IDC_STATISTICS,12,12,230,204 PUSHBUTTON "Remise 0 des statistiques",IDC_BUT_RESET_STATS,156, 238,92,14 LTEXT "Dernire remise 0 :",IDC_STATIC,7,241,36,8 LTEXT "<<>>",IDC_LAST_RESET_DATE,47,241,107,8 END IDD_MANAGER DIALOGEX 0, 0, 275, 308 STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "SpamBayes Manager" FONT 8, "Tahoma", 0, 0, 0x1 BEGIN DEFPUSHBUTTON "Fermer",IDOK,216,287,50,14 PUSHBUTTON "Annuler",IDCANCEL,155,287,50,14,NOT WS_VISIBLE CONTROL "",IDC_TAB,"SysTabControl32",0x0,8,7,258,276 PUSHBUTTON "A propos...",IDC_ABOUT_BTN,8,287,50,14 END IDD_FILTER_SPAM DIALOGEX 0, 0, 251, 147 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION | WS_SYSMENU CAPTION "Spam" FONT 8, "Tahoma", 400, 0, 0x1 BEGIN LTEXT "Dossiers filtrer lors de l'arrive de nouveaux messages", IDC_STATIC,8,9,168,11 CONTROL "Folder names...\nLine 2",IDC_FOLDER_WATCH,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,7,20,177,12 PUSHBUTTON "&Parcourir...",IDC_BROWSE_WATCH,194,19,50,14 GROUPBOX "Spam sr",IDC_STATIC,7,43,237,80 LTEXT "Pour tre considr comme un spam, un message doit obtenir une note d'au moins", IDC_STATIC,13,52,212,10 CONTROL "",IDC_SLIDER_CERTAIN,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,13,62,165,22 EDITTEXT IDC_EDIT_CERTAIN,184,63,51,14,ES_AUTOHSCROLL LTEXT "et ces messages doivent tre :",IDC_STATIC,13,82,107,10 COMBOBOX IDC_ACTION_CERTAIN,13,93,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "dans le dossier",IDC_STATIC,75,95,31,10 CONTROL "Folder names...",IDC_FOLDER_CERTAIN,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,120,93,59,14 PUSHBUTTON "P&arcourir...",IDC_BROWSE_CERTAIN,184,93,50,14 CONTROL "Marquer les messages comme &lus",IDC_MARK_SPAM_AS_READ, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,110,81,10 END IDD_FILTER_UNSURE DIALOGEX 0, 0, 249, 124 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION | WS_SYSMENU CAPTION "Messages douteux" FONT 8, "Tahoma", 400, 0, 0x1 BEGIN LTEXT "Pour tre considr comme douteux, un message doit obtenir une note d'au moins", IDC_STATIC,12,11,212,10 CONTROL "",IDC_SLIDER_UNSURE,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,12,18,165,20 EDITTEXT IDC_EDIT_UNSURE,183,24,54,14,ES_AUTOHSCROLL LTEXT "et ces messages doivent tre :",IDC_STATIC,12,38,107,10 COMBOBOX IDC_ACTION_UNSURE,12,49,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "dans le dossier",IDC_STATIC,74,52,31,10 CONTROL "(folder name)",IDC_FOLDER_UNSURE,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,119,49,59,14 PUSHBUTTON "Pa&rcourir",IDC_BROWSE_UNSURE,183,49,50,14 CONTROL "Marquer les messages l&us",IDC_MARK_UNSURE_AS_READ, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,70,101,10 END IDD_DIAGNOSTIC DIALOGEX 0, 0, 201, 98 STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Diagnostiques" FONT 8, "Tahoma" BEGIN LTEXT "Ces options avances sont fournies des fins de diagnostiques et dboguage seulement. Vous ne devriez changer les valeurs que sur demande ou si vous savez exactement ce que vous faites.", IDC_STATIC,5,3,192,36 LTEXT "Verbosit du log",IDC_STATIC,5,44,56,8 EDITTEXT IDC_VERBOSE_LOG,73,42,40,14,ES_AUTOHSCROLL PUSHBUTTON "Voir le fichier de log...",IDC_BUT_VIEW_LOG,122,41,75, 14 CONTROL "Enregistrer la note attribue",IDC_SAVE_SPAM_SCORE, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,5,63,72,10 PUSHBUTTON "Annuler",IDCANCEL,69,79,50,14,NOT WS_VISIBLE DEFPUSHBUTTON "Fermer",IDOK,147,79,50,14 END IDD_WIZARD DIALOGEX 0, 0, 384, 190 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Assistant de configuration SpamBayes" FONT 8, "Tahoma" BEGIN PUSHBUTTON "Annuler",IDCANCEL,328,173,50,14 PUSHBUTTON "<< Prcdent",IDC_BACK_BTN,204,173,50,14 DEFPUSHBUTTON "Suivant>>,Fin",IDC_FORWARD_BTN,259,173,52,14 CONTROL "",IDC_PAGE_PLACEHOLDER,"Static",SS_ETCHEDFRAME,75,4,303, 167 CONTROL 125,IDC_WIZ_GRAPHIC,"Static",SS_BITMAP,0,0,69,190 END IDD_WIZARD_WELCOME DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma" BEGIN LTEXT "Bienvenue dans l'assistant de paramtrage de SpamBayes", IDC_STATIC,20,4,191,14 LTEXT "Cet assistant va vous guider dans le paramtrage du module SpamBayes pour Outlook. Merci de prciser o vous en tes pour le paramtrage.", IDC_STATIC,20,20,255,18 CONTROL "Je n'ai rien prpar du tout pour SpamBayes.", IDC_BUT_PREPARATION,"Button",BS_AUTORADIOBUTTON | BS_TOP | WS_GROUP,20,42,190,11 CONTROL "J'ai dj filtr les bon messages (ham) et les mauvais (spam) dans des dossiers spars adapts l'entranement.", IDC_STATIC,"Button",BS_AUTORADIOBUTTON | BS_TOP | BS_MULTILINE,20,59,255,18 CONTROL "Je prfre me dbrouiller tout seul pour configurer SpamBayes.", IDC_STATIC,"Button",BS_AUTORADIOBUTTON | BS_TOP,20,82, 187,12 LTEXT "Pour plus d'informations sur l'entranement et le paramtrage de SpamBayes, cliquer sur le bouton A propos.", IDC_STATIC,20,103,185,26 PUSHBUTTON "A propos...",IDC_BUT_ABOUT,215,104,60,15 LTEXT "Si vous quittez l'assistant, vous pouvez le relancer partir du SpamBayes Manager, disponible sur la barre d'outil SpamBayes.", IDC_STATIC,20,137,232,17 END IDD_WIZARD_FINISHED_UNTRAINED DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Bravo !",IDC_STATIC,20,4,247,14 LTEXT "SpamBayes est maintenant paramtr et prt filtrer sur vos messages", IDC_STATIC,20,22,247,16 LTEXT "Comme SpamBayes ne s'est pas encore entran, tous les messages vont tre rangs dans le dossier Douteux (Unsure). Pour chacun des messages, vous devez cliquer soit sur 'C'est du Spam' soit sur 'Ce n'est pas du Spam'.", IDC_STATIC,20,42,247,27 LTEXT "Pour acclrer l'entranement, vous pouvez dplacer manuellement tous les spams de votre 'Bote de rception' dans le dossier 'Spam', et alors slectionner 'Entranement' depuis le SpamBayes manager.", IDC_STATIC,20,83,247,31 LTEXT "Plus le programme s'entrane et plus la fiabilit augmente. Notez qu'aprs seulement quelques messages le rsultat est tonnant.", IDC_STATIC,20,69,247,15 LTEXT "Cliquer sur Fin pour sortir de l'assistant.",IDC_STATIC, 20,121,148,9 END IDD_WIZARD_FOLDERS_REST DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN PUSHBUTTON "Parcourir...",IDC_BROWSE_SPAM,208,85,60,15 LTEXT "Dossiers Spam et Douteux",IDC_STATIC,20,4,247,14 LTEXT "SpamBayes utilise deux dossiers pour grer le spam. Un dossier 'sr' pour stocker le spam et un dossier 'douteux' qu'il vous faudra aiguiller manuellement.", IDC_STATIC,20,20,247,22 LTEXT "Si vous entrez un nom de dossier qui n'existe pas, il va tre cr automatiquement. Pour choisir un dossier existant, cliquer sur Parcourir.", IDC_STATIC,20,44,243,24 EDITTEXT IDC_FOLDER_CERTAIN,20,85,179,14,ES_AUTOHSCROLL LTEXT "Les messages douteux vont tre rangs dans le dossier nomm", IDC_STATIC,20,105,186,12 EDITTEXT IDC_FOLDER_UNSURE,20,117,177,14,ES_AUTOHSCROLL LTEXT "Les messages spam vont tre rangs dans le dossier nomm", IDC_STATIC,20,72,137,8 PUSHBUTTON "Parcourir...",IDC_BROWSE_UNSURE,208,117,60,15 END IDD_WIZARD_FOLDERS_WATCH DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN PUSHBUTTON "Parcourir...",IDC_BROWSE_WATCH,225,134,50,14 LTEXT "Dossiers recevant les nouveaux messages",IDC_STATIC,20, 4,247,14 LTEXT "SpamBayes a besoin de connaitre les dossiers utiliss pour rceptionner les nouveaux messages. En gnral, il s'agit du dossier 'Bote de rception', mais vous pouvez en prciser d'autres filtrer.", IDC_STATIC,20,21,247,25 LTEXT "Les dossiers suivants seront filtrs. Uiliser le bouton Parcourir pour changer la liste puis cliquer sur Suivant.", IDC_STATIC,20,79,247,20 LTEXT "Astuce : si vous utilisez des rgles d'aiguillage de messages, vous devriez ajouter les dossiers destination la liste.", IDC_STATIC,20,51,241,20 EDITTEXT IDC_FOLDER_WATCH,20,100,195,48,ES_MULTILINE | ES_AUTOHSCROLL | ES_READONLY END IDD_WIZARD_FINISHED_UNCONFIGURED DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Annulation du paramtrage",IDC_STATIC,20,4,247,14 LTEXT "L'cran principal de SpamBayes va maintenant tre affich. Vous devez dfinir les dossiers et activer SpamBayes pour commencer filtrer les messages.", IDC_STATIC,20,29,247,16 LTEXT "Cliquer sur Fin pour quitter l'assistant.",IDC_STATIC, 20,139,148,9 END IDD_WIZARD_FOLDERS_TRAIN DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN PUSHBUTTON "Parcourir...",IDC_BROWSE_HAM,208,49,60,15 LTEXT "Entranement",IDC_STATIC,20,4,247,10 LTEXT "Slectionner les dossiers contenant les messages pr-tris, un pour les spams et un pour les bons messages.", IDC_STATIC,20,16,243,16 EDITTEXT IDC_FOLDER_HAM,20,49,179,14,ES_AUTOHSCROLL | ES_READONLY LTEXT "Des exemples de messages spams ou indsirables figurent dans le dossier", IDC_STATIC,20,71,198,8 EDITTEXT IDC_FOLDER_CERTAIN,20,81,177,14,ES_AUTOHSCROLL | ES_READONLY LTEXT "Des exemples de bons messages figurent dans le dossier", IDC_STATIC,20,38,153,8 PUSHBUTTON "Parcourir...",IDC_BROWSE_SPAM,208,81,60,15 LTEXT "Si vous n'avez pas de messages pr-tris ou que vous avez dj pratiqu l'entranement ou voulez garder la base, cliquer sur Prcdent et choisissez l'option 'Je n'ai rien prpar du tout'.", IDC_STATIC,20,128,243,26 CONTROL "Attribuer une note aux messages lorsque l'entranement est termin.", IDC_BUT_RESCORE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,20, 108,163,16 END IDD_WIZARD_TRAIN DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Entranement",-1,20,4,247,14 LTEXT "SpamBayes s'entrane sur vos bons messages et sur les spams.", -1,20,22,247,16 CONTROL "",IDC_PROGRESS,"msctls_progress32",WS_BORDER,20,45,255, 11 LTEXT "(progress text)",IDC_PROGRESS_TEXT,20,61,257,10 END IDD_WIZARD_FINISHED_TRAINED DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Bravo !",IDC_STATIC,20,4,247,14 LTEXT "SpamBayes s'est entran et est maintenant paramtr. Les premiers rsultats sont observables ds maintenant !", IDC_TRAINING_STATUS,20,35,247,26 LTEXT "Bien que SpamBayes ce soit entran, il continue apprendre. Pensez rgulirement vrifier le contenu du dossier 'Douteux', et utilisez les boutons 'C'est du spam' et 'Ce n'est pas du spam'.", IDC_STATIC,20,68,249,30 LTEXT "Cliquer sur Fin pour fermer l'assistant.",IDC_STATIC,20, 104,148,9 END IDD_WIZARD_TRAINING_IS_IMPORTANT DIALOGEX 0, 0, 328, 156 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma" BEGIN LTEXT "SpamBayes ne fonctionnera pas tant qu'il ne s'est pas entran.", IDC_STATIC,11,8,312,14 PUSHBUTTON "A propos de l'entranement...",IDC_BUT_ABOUT,258,135,65, 15 LTEXT "SpamBayes est un systme qui apprend reconnatre les bons et les mauvais messages partir des exemples que vous lui donnez. A la base, il ne dispose d'aucun filtres, il doit donc tre entran pour devenir effectif.", IDC_STATIC,11,21,312,30 LTEXT "Pour commencer, SpamBayes va aiguiller tous vos messages dans le dossier 'Douteux'. L'entranement est simple : pour chaque message, vous spcifiez alors s'il s'agit de spam ou non partir des boutons 'C'est du spam' et 'Ce n'est pas du spam'. Petit pete????ctls_progress32", IDC_STATIC,22,61,301,35 LTEXT "Cette option fermera l'assistant et vous dire comment aiguiller vos messages. Vous pourrez paramtrer SpamBayes et le rendre actif immdiatement sur vos messages", IDC_STATIC,22,113,301,27 LTEXT "Pour plus d'information, cliquer sur le bouton A propos de l'entranement.", IDC_STATIC,11,137,234,8 CONTROL "Je veux stopper l'entranement et laisser SpamBayes apprendre sur les nouveaux messages", IDC_BUT_UNTRAINED,"Button",BS_AUTORADIOBUTTON | WS_GROUP, 11,50,312,11 CONTROL "Je vais effectuer le pr-tri moi-mme (bon / spam) et paramtrer SpamBayes plus tard", IDC_BUT_TRAIN,"Button",BS_AUTORADIOBUTTON,11,98,312,11 END IDD_WIZARD_FINISHED_TRAIN_LATER DIALOGEX 0, 0, 284, 162 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION EXSTYLE WS_EX_CONTEXTHELP FONT 8, "Tahoma", 0, 0, 0x1 BEGIN LTEXT "Paramtrage abandonn",IDC_STATIC,20,4,247,14 LTEXT "Pour effectuer l'entranement initial, vous devriez crer deux dossiers, un contenant de bons messages et un autre des messages non sollicits.", IDC_STATIC,20,17,247,27 LTEXT "Cliquer sur Fin pour quitter l'assistant.",IDC_STATIC, 20,145,148,9 LTEXT "Pour des exemples de bons messages, vous pouvez utiliser votre 'Bote de rception' mais vous evez tre SR qu'elle ne contient aucun message non sollicit", IDC_STATIC,20,42,247,26 LTEXT "Si faire ce tri tait trop fastidieux, crez simplement un dossier temporaire en mettant quelques messages en exemple.", IDC_STATIC,20,58,247,17 LTEXT "Pour des exemples de messages non sollicits vous pouvez utiliser le dossier 'Elments supprims'. Si faire ce tri tait trop fastidieux, crez simplement un dossier temporaire en mettant quelques messages en exemple.", IDC_STATIC,20,80,247,35 LTEXT "Lorsque vous aurez termin, ouvrez le SpamBayes Manager via la barre d'outil SpamBayes, et redmarrez l'assistant.", IDC_STATIC,20,121,245,17 END IDD_NOTIFICATIONS DIALOGEX 0, 0, 248, 257 STYLE DS_SETFONT | WS_CHILD | WS_CAPTION CAPTION "Notifications" FONT 8, "Tahoma", 0, 0, 0x0 BEGIN GROUPBOX "New Mail Sounds",IDC_STATIC,7,3,241,229 CONTROL "Enable new mail notification sounds",IDC_ENABLE_SOUNDS, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,14,17,129,10 LTEXT "Good sound:",IDC_STATIC,14,31,42,8 EDITTEXT IDC_HAM_SOUND,14,40,174,14,ES_AUTOHSCROLL PUSHBUTTON "Browse...",IDC_BROWSE_HAM_SOUND,192,40,50,14 LTEXT "Unsure sound:",IDC_STATIC,14,58,48,8 EDITTEXT IDC_UNSURE_SOUND,14,67,174,14,ES_AUTOHSCROLL PUSHBUTTON "Browse...",IDC_BROWSE_UNSURE_SOUND,192,67,50,14 LTEXT "Spam sound:",IDC_STATIC,14,85,42,8 EDITTEXT IDC_SPAM_SOUND,14,94,174,14,ES_AUTOHSCROLL PUSHBUTTON "Browse...",IDC_BROWSE_SPAM_SOUND,192,94,50,14 LTEXT "Time to wait for additional messages:",IDC_STATIC,14, 116,142,8 CONTROL "",IDC_ACCUMULATE_DELAY_SLIDER,"msctls_trackbar32", TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,14,127,148,22 EDITTEXT IDC_ACCUMULATE_DELAY_TEXT,163,133,40,14,ES_AUTOHSCROLL LTEXT "seconds",IDC_STATIC,205,136,28,8 END IDD_GENERAL DIALOGEX 0, 0, 253, 257 STYLE DS_MODALFRAME | WS_CHILD | WS_VISIBLE | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "General" FONT 8, "Tahoma" BEGIN LTEXT "SpamBayes Version",IDC_VERSION,6,54,242,8 LTEXT "SpamBayes a besoin de s'entraner avant d'tre activ. Cliquer sur l'onglet 'Entranement', ou utilisez l'assistant en vous laissant guider.", IDC_STATIC,6,67,242,17 LTEXT "Status de la base d'entranement :",IDC_STATIC,6,90,222, 8 LTEXT "123 spams ; 456 bons messages\r\nLine2\r\nLine3", IDC_TRAINING_STATUS,6,101,242,27,SS_SUNKEN CONTROL "Activer SpamBayes",IDC_BUT_FILTER_ENABLE,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,6,221,97,11 LTEXT "Les messages classifis comme spam sont aiguills dans le dossier Folder1\nLes messages douteux sont galement aiguills", IDC_FILTER_STATUS,6,146,242,67,SS_SUNKEN PUSHBUTTON "Revenir au paramtrage initial...",IDC_BUT_RESET,6,238, 109,14 PUSHBUTTON "Assistant...",IDC_BUT_WIZARD,142,238,106,15 LTEXT "Status des filtres :",IDC_STATIC,6,135,222,8 CONTROL 1062,IDC_LOGO_GRAPHIC,"Static",SS_BITMAP | SS_REALSIZEIMAGE,0,2,275,52 END IDD_TRAINING DIALOGEX 0, 0, 252, 257 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Entranement" FONT 8, "Tahoma" BEGIN GROUPBOX "",IDC_STATIC,5,1,243,113 LTEXT "Dossiers contenant les bons messages",IDC_STATIC,11,11, 124,8 CONTROL "",IDC_STATIC_HAM,"Static",SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN,11,21,175,12 PUSHBUTTON "&Parcourir...",IDC_BROWSE_HAM,192,20,50,14 LTEXT "Dossiers contenant les messages non sollicits", IDC_STATIC,11,36,171,9 CONTROL "Static",IDC_STATIC_SPAM,"Static",SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,11,46,174,12 PUSHBUTTON "P&arcourir...",IDC_BROWSE_SPAM,192,46,50,14 CONTROL "Attribuer une note aux messages aprs l'entranement", IDC_BUT_RESCORE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,11, 64,111,10 CONTROL "&Reconstruire toute la base",IDC_BUT_REBUILD,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,137,64,92,10 CONTROL "Progress1",IDC_PROGRESS,"msctls_progress32",WS_BORDER, 11,76,231,11 PUSHBUTTON "&Commencer l'entranement",IDC_START,11,91,90,14, BS_NOTIFY LTEXT "status entranement status entranement status entranement status entranement status entranements status entranement status entranement", IDC_PROGRESS_TEXT,106,89,135,17 GROUPBOX "Entranement incremental",IDC_STATIC,4,117,244,87 CONTROL "Dplacer un message d'un dossier spam la 'Bote de rception' participe l'entranement.", IDC_BUT_TRAIN_FROM_SPAM_FOLDER,"Button",BS_AUTOCHECKBOX | BS_MULTILINE | WS_TABSTOP,11,127,204,18 LTEXT "Lors d'un click sur 'Ce n'est pas du spam'",IDC_STATIC, 10,148,129,8 COMBOBOX IDC_RECOVER_RS,142,145,99,54,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP CONTROL "Dplacer un message d'un dossier de la 'Bote de rception' au dossier 'Spam' participe l'entranement.", IDC_BUT_TRAIN_TO_SPAM_FOLDER,"Button",BS_AUTOCHECKBOX | BS_MULTILINE | WS_TABSTOP,11,163,204,16 LTEXT "Lors d'un click sur 'C'est du spam'",IDC_STATIC,10,183, 106,8 COMBOBOX IDC_DEL_SPAM_RS,127,180,114,54,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP END IDD_FILTER_NOW DIALOGEX 0, 0, 244, 185 STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Filtrer maintenant" FONT 8, "Tahoma" BEGIN LTEXT "Filtrer les dossiers suivants",IDC_STATIC,8,9,168,11 CONTROL "Dossiers...\nLine 2",IDC_FOLDER_NAMES,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN,7,20,172, 12 PUSHBUTTON "Parcourir...",IDC_BROWSE,187,19,50,14 GROUPBOX "Filtres et actions",IDC_STATIC,7,38,230,40,WS_GROUP CONTROL "Effectuer les actions (aiguillage du message)", IDC_BUT_ACT_ALL,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,15,49,126,10 CONTROL "Attribuer une note mais ne pas effectuer d'action", IDC_BUT_ACT_SCORE,"Button",BS_AUTORADIOBUTTON,15,62,203, 10 GROUPBOX "Restrendre le filtre",IDC_STATIC,7,84,230,35,WS_GROUP CONTROL "Aux messages non lus",IDC_BUT_UNREAD,"Button", BS_AUTOCHECKBOX | WS_TABSTOP,15,94,149,9 CONTROL "Aux messages qui n'ont pas eu de note attribue", IDC_BUT_UNSEEN,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,15, 106,149,9 CONTROL "Progress1",IDC_PROGRESS,"msctls_progress32",WS_BORDER,7, 129,230,11 LTEXT "Static",IDC_PROGRESS_TEXT,7,144,227,10 DEFPUSHBUTTON "Dmarrer le filtrage",IDC_START,7,161,67,14 PUSHBUTTON "Fermer",IDCANCEL,187,162,50,14 END IDD_FILTER DIALOGEX 0, 0, 249, 257 STYLE DS_MODALFRAME | WS_CHILD | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CONTEXTHELP CAPTION "Filtrage" FONT 8, "Tahoma" BEGIN LTEXT "Filtrer les dossiers suivant lors de la rception de nouveaux messages", IDC_STATIC,8,4,168,11 CONTROL "Dossiers...\nLine 2",IDC_FOLDER_WATCH,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,7,16,177,12 PUSHBUTTON "Parcourir...",IDC_BROWSE_WATCH,192,14,50,14 GROUPBOX "Spam sr",IDC_STATIC,7,33,235,80 LTEXT "Pour tre considr comme un spam, un message doit obtenir une note d'au moins", IDC_STATIC,13,42,212,10 CONTROL "Slider1",IDC_SLIDER_CERTAIN,"msctls_trackbar32", TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,13,52,165,22 EDITTEXT IDC_EDIT_CERTAIN,184,53,51,14,ES_AUTOHSCROLL LTEXT "et ces messages doivent tre :",IDC_STATIC,13,72,107,10 COMBOBOX IDC_ACTION_CERTAIN,12,83,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "dans le dossier",IDC_STATIC,71,85,28,10 CONTROL "Folder names...",IDC_FOLDER_CERTAIN,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,102,83,77,14 PUSHBUTTON "&Parcourir",IDC_BROWSE_CERTAIN,184,83,50,14 GROUPBOX "Message douteux",IDC_STATIC,6,117,235,81 LTEXT "Pour tre considr comme douteux, un message doit obtenir une note d'au moins", IDC_STATIC,12,128,212,10 CONTROL "Slider1",IDC_SLIDER_UNSURE,"msctls_trackbar32", TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,12,135,165,20 EDITTEXT IDC_EDIT_UNSURE,183,141,54,14,ES_AUTOHSCROLL LTEXT "et ces messages doivent tre :",IDC_STATIC,12,155,107, 10 COMBOBOX IDC_ACTION_UNSURE,12,166,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "dans le dossier",IDC_STATIC,71,169,48,8 CONTROL "(folder name)",IDC_FOLDER_UNSURE,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,122,166,57,14 PUSHBUTTON "P&arcourir",IDC_BROWSE_UNSURE,184,166,50,14 CONTROL "Marquer les spams comme lus",IDC_MARK_SPAM_AS_READ, "Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,100,81,10 CONTROL "Marquer les messages douteux comme lus", IDC_MARK_UNSURE_AS_READ,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,186,101,10 GROUPBOX "Bons messages",IDC_STATIC,6,203,235,48 LTEXT "Ces messages doivent tre :",IDC_STATIC,12,215,107,10 COMBOBOX IDC_ACTION_HAM,12,228,55,40,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP LTEXT "dans le dossier",IDC_STATIC,71,230,48,8 CONTROL "(folder name)",IDC_FOLDER_HAM,"Static", SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_SUNKEN | WS_GROUP,122,228,57,14 PUSHBUTTON "Pa&rcourir...",IDC_BROWSE_HAM,184,228,50,14 END IDD_FOLDER_SELECTOR DIALOG DISCARDABLE 0, 0, 253, 215 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Dialog" FONT 8, "Tahoma" BEGIN LTEXT "&Dossiers :",IDC_STATIC,7,7,47,9 CONTROL "",IDC_LIST_FOLDERS,"SysTreeView32",TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | TVS_DISABLEDRAGDROP | TVS_SHOWSELALWAYS | TVS_CHECKBOXES | WS_BORDER | WS_TABSTOP,7,21,172,140 CONTROL "(sub)",IDC_BUT_SEARCHSUB,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,7,167,126,9 LTEXT "(status1)",IDC_STATUS1,7,180,220,9 LTEXT "(status2)",IDC_STATUS2,7,194,220,9 DEFPUSHBUTTON "OK",IDOK,190,21,57,14 PUSHBUTTON "Annuler",IDCANCEL,190,39,57,14 PUSHBUTTON "&Tout effacer",IDC_BUT_CLEARALL,190,58,57,14 PUSHBUTTON "&Nouveau dossier",IDC_BUT_NEW,190,77,58,14 END ///////////////////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO MOVEABLE PURE BEGIN IDD_ADVANCED, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 241 VERTGUIDE, 16 BOTTOMMARGIN, 204 END IDD_MANAGER, DIALOG BEGIN BOTTOMMARGIN, 253 END IDD_FILTER_SPAM, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 244 TOPMARGIN, 7 BOTTOMMARGIN, 140 END IDD_FILTER_UNSURE, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 242 TOPMARGIN, 7 BOTTOMMARGIN, 117 END IDD_DIAGNOSTIC, DIALOG BEGIN LEFTMARGIN, 5 RIGHTMARGIN, 197 BOTTOMMARGIN, 93 END IDD_WIZARD, DIALOG BEGIN RIGHTMARGIN, 378 END IDD_WIZARD_WELCOME, DIALOG BEGIN VERTGUIDE, 20 VERTGUIDE, 275 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_UNTRAINED, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FOLDERS_REST, DIALOG BEGIN VERTGUIDE, 20 VERTGUIDE, 268 BOTTOMMARGIN, 161 HORZGUIDE, 85 HORZGUIDE, 117 END IDD_WIZARD_FOLDERS_WATCH, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_UNCONFIGURED, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FOLDERS_TRAIN, DIALOG BEGIN VERTGUIDE, 20 VERTGUIDE, 268 BOTTOMMARGIN, 161 HORZGUIDE, 49 HORZGUIDE, 81 END IDD_WIZARD_TRAIN, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_FINISHED_TRAINED, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END IDD_WIZARD_TRAINING_IS_IMPORTANT, DIALOG BEGIN VERTGUIDE, 11 VERTGUIDE, 22 VERTGUIDE, 323 BOTTOMMARGIN, 155 END IDD_WIZARD_FINISHED_TRAIN_LATER, DIALOG BEGIN VERTGUIDE, 20 BOTTOMMARGIN, 161 END END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Bitmap // IDB_SBLOGO BITMAP MOVEABLE PURE "sblogo.bmp" IDB_SBWIZLOGO BITMAP MOVEABLE PURE "sbwizlogo.bmp" IDB_FOLDERS BITMAP MOVEABLE PURE "folders.bmp" #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE MOVEABLE PURE BEGIN "dialogs.h\0" END 2 TEXTINCLUDE MOVEABLE PURE BEGIN "#include ""winres.h""\r\n" "// spambayes dialog definitions\r\n" "\0" END 3 TEXTINCLUDE MOVEABLE PURE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED #endif // English (U.S.) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED spambayes-1.1a6/spambayes/languages/fr/DIALOGS/i18n_dialogs.py0000775000076500000240000007011210646440125024145 0ustar skipstaff00000000000000#c:\spambayes\languages\fr_FR\DIALOGS\i18n_dialogs.py #This is a generated file. Please edit c:\spambayes\languages\fr_FR\DIALOGS\dialogs.rc instead. _rc_size_=33774 _rc_mtime_=1112074577 try: _ except NameError: def _(s): return s class FakeParser: dialogs = {'IDD_MANAGER': [[_('SpamBayes Manager'), (0, 0, 275, 308), -1865940928, 1024, (8, 'Tahoma')], [128, _('Fermer'), 1, (216, 287, 50, 14), 1342177281], [128, _('Annuler'), 2, (155, 287, 50, 14), 1073741824], ['SysTabControl32', '', 1068, (8, 7, 258, 276), 1342177280], [128, _('A propos...'), 1072, (8, 287, 50, 14), 1342177280]], 'IDD_DIAGNOSTIC': [[_('Diagnostiques'), (0, 0, 201, 98), -1865940928, 1024, (8, 'Tahoma')], [130, _('Ces options avanc\xe9es sont fournies \xe0 des fins de diagnostiques et d\xe9boguage seulement. Vous ne devriez changer les valeurs que sur demande ou si vous savez exactement ce que vous faites.'), -1, (5, 3, 192, 36), 1342177280], [130, _('Verbosit\xe9 du log'), -1, (5, 44, 56, 8), 1342177280], [129, '', 1061, (73, 42, 40, 14), 1350566016], [128, _('Voir le fichier de log...'), 1093, (122, 41, 75, 14), 1342177280], [128, _('Enregistrer la note attribu\xe9e'), 1048, (5, 63, 72, 10), 1342242819], [128, _('Annuler'), 2, (69, 79, 50, 14), 1073741824], [128, _('Fermer'), 1, (147, 79, 50, 14), 1342177281]], 'IDD_FILTER_SPAM': [[_('Spam'), (0, 0, 251, 147), 1355284672, None, (8, 'Tahoma')], [130, _("Dossiers \xe0 filtrer lors de l'arriv\xe9e de nouveaux messages"), -1, (8, 9, 168, 11), 1342177280], [130, _('Folder names...\\nLine 2'), 1038, (7, 20, 177, 12), 1342312972], [128, _('&Parcourir...'), 1039, (194, 19, 50, 14), 1342177280], [128, _('Spam s\xfbr'), -1, (7, 43, 237, 80), 1342177287], [130, _("Pour \xeatre consid\xe9r\xe9 comme un spam, un message doit obtenir une note d'au moins"), -1, (13, 52, 212, 10), 1342177280], ['msctls_trackbar32', '', 1023, (13, 62, 165, 22), 1342242821], [129, '', 1024, (184, 63, 51, 14), 1350566016], [130, _('et ces messages doivent \xeatre :'), -1, (13, 82, 107, 10), 1342177280], [133, '', 1025, (13, 93, 55, 40), 1344339971], [130, _('dans le dossier'), -1, (75, 95, 31, 10), 1342177280], [130, _('Folder names...'), 1027, (120, 93, 59, 14), 1342312972], [128, _('P&arcourir...'), 1028, (184, 93, 50, 14), 1342177280], [128, _('Marquer les messages comme &lus'), 1047, (13, 110, 81, 10), 1342242819]], 'IDD_TRAINING': [[_('Entra\xeenement'), (0, 0, 252, 257), 1355284672, 1024, (8, 'Tahoma')], [128, '', -1, (5, 1, 243, 113), 1342177287], [130, _('Dossiers contenant les bons messages'), -1, (11, 11, 124, 8), 1342177280], [130, '', 1002, (11, 21, 175, 12), 1342181900], [128, _('&Parcourir...'), 1004, (192, 20, 50, 14), 1342177280], [130, _('Dossiers contenant les messages non sollicit\xe9s'), -1, (11, 36, 171, 9), 1342177280], [130, _('Static'), 1003, (11, 46, 174, 12), 1342312972], [128, _('P&arcourir...'), 1005, (192, 46, 50, 14), 1342177280], [128, _("Attribuer une note aux messages apr\xe8s l'entra\xeenement"), 1008, (11, 64, 111, 10), 1342242819], [128, _('&Reconstruire toute la base'), 1007, (137, 64, 92, 10), 1342242819], ['msctls_progress32', _('Progress1'), 1000, (11, 76, 231, 11), 1350565888], [128, _("&Commencer l'entra\xeenement"), 1006, (11, 91, 90, 14), 1342193664], [130, _('status entra\xeenement status entra\xeenement status entra\xeenement status entra\xeenement status entra\xeenements status entra\xeenement status entra\xeenement'), 1001, (106, 89, 135, 17), 1342177280], [128, _('Entra\xeenement incremental'), -1, (4, 117, 244, 87), 1342177287], [128, _("D\xe9placer un message d'un dossier spam \xe0 la 'Bo\xeete de r\xe9ception' participe \xe0 l'entra\xeenement."), 1010, (11, 127, 204, 18), 1342251011], [130, _("Lors d'un click sur 'Ce n'est pas du spam'"), -1, (10, 148, 129, 8), 1342177280], [133, '', 1075, (142, 145, 99, 54), 1344339971], [128, _("D\xe9placer un message d'un dossier de la 'Bo\xeete de r\xe9ception' au dossier 'Spam' participe \xe0 l'entra\xeenement."), 1011, (11, 163, 204, 16), 1342251011], [130, _("Lors d'un click sur 'C'est du spam'"), -1, (10, 183, 106, 8), 1342177280], [133, '', 1074, (127, 180, 114, 54), 1344339971]], 'IDD_WIZARD': [[_('Assistant de configuration SpamBayes'), (0, 0, 384, 190), -1865940800, 1024, (8, 'Tahoma')], [128, _('Annuler'), 2, (328, 173, 50, 14), 1342177280], [128, _('<< Pr\xe9c\xe9dent'), 1069, (204, 173, 50, 14), 1342177280], [128, _('Suivant>>,Fin'), 1077, (259, 173, 52, 14), 1342177281], [130, '', 1078, (75, 4, 303, 167), 1342177298], [130, '125', 1092, (0, 0, 69, 190), 1342177294]], 'IDD_WIZARD_FOLDERS_WATCH': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [128, _('Parcourir...'), 1039, (225, 134, 50, 14), 1342177280], [130, _('Dossiers recevant les nouveaux messages'), -1, (20, 4, 247, 14), 1342177280], [130, _("SpamBayes a besoin de connaitre les dossiers utilis\xe9s pour r\xe9ceptionner les nouveaux messages. En g\xe9n\xe9ral, il s'agit du dossier 'Bo\xeete de r\xe9ception', mais vous pouvez en pr\xe9ciser d'autres \xe0 filtrer."), -1, (20, 21, 247, 25), 1342177280], [130, _('Les dossiers suivants seront filtr\xe9s. Uiliser le bouton Parcourir pour changer la liste puis cliquer sur Suivant.'), -1, (20, 79, 247, 20), 1342177280], [130, _("Astuce : si vous utilisez des r\xe8gles d'aiguillage de messages, vous devriez ajouter les dossiers destination \xe0 la liste."), -1, (20, 51, 241, 20), 1342177280], [129, '', 1038, (20, 100, 195, 48), 1350568068]], 'IDD_WIZARD_FINISHED_TRAINED': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('Bravo !'), -1, (20, 4, 247, 14), 1342177280], [130, _("SpamBayes s'est entra\xeen\xe9 et est maintenant param\xe9tr\xe9. Les premiers r\xe9sultats sont observables d\xe8s maintenant !"), 1035, (20, 35, 247, 26), 1342177280], [130, _("Bien que SpamBayes ce soit entra\xeen\xe9, il continue \xe0 apprendre. Pensez \xe0 r\xe9guli\xe8rement v\xe9rifier le contenu du dossier 'Douteux', et utilisez les boutons 'C'est du spam' et 'Ce n'est pas du spam'."), -1, (20, 68, 249, 30), 1342177280], [130, _("Cliquer sur Fin pour fermer l'assistant."), -1, (20, 104, 148, 9), 1342177280]], 'IDD_WIZARD_FOLDERS_TRAIN': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [128, _('Parcourir...'), 1004, (208, 49, 60, 15), 1342177280], [130, _('Entra\xeenement'), -1, (20, 4, 247, 10), 1342177280], [130, _('S\xe9lectionner les dossiers contenant les messages pr\xe9-tri\xe9s, un pour les spams et un pour les bons messages.'), -1, (20, 16, 243, 16), 1342177280], [129, '', 1083, (20, 49, 179, 14), 1350568064], [130, _('Des exemples de messages spams ou ind\xe9sirables figurent dans le dossier'), -1, (20, 71, 198, 8), 1342177280], [129, '', 1027, (20, 81, 177, 14), 1350568064], [130, _('Des exemples de bons messages figurent dans le dossier'), -1, (20, 38, 153, 8), 1342177280], [128, _('Parcourir...'), 1005, (208, 81, 60, 15), 1342177280], [130, _("Si vous n'avez pas de messages pr\xe9-tri\xe9s ou que vous avez d\xe9j\xe0 pratiqu\xe9 l'entra\xeenement ou voulez garder la base, cliquer sur Pr\xe9c\xe9dent et choisissez l'option 'Je n'ai rien pr\xe9par\xe9 du tout'."), -1, (20, 128, 243, 26), 1342177280], [128, _("Attribuer une note aux messages lorsque l'entra\xeenement est termin\xe9."), 1008, (20, 108, 163, 16), 1342242819]], 'IDD_WIZARD_TRAIN': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('Entra\xeenement'), -1, (20, 4, 247, 14), 1342177280], [130, _("SpamBayes s'entra\xeene sur vos bons messages et sur les spams."), -1, (20, 22, 247, 16), 1342177280], ['msctls_progress32', '', 1000, (20, 45, 255, 11), 1350565888], [130, _('(progress text)'), 1001, (20, 61, 257, 10), 1342177280]], 'IDD_WIZARD_FINISHED_TRAIN_LATER': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('Param\xe9trage abandonn\xe9'), -1, (20, 4, 247, 14), 1342177280], [130, _("Pour effectuer l'entra\xeenement initial, vous devriez cr\xe9er deux dossiers, un contenant de bons messages et un autre des messages non sollicit\xe9s."), -1, (20, 17, 247, 27), 1342177280], [130, _("Cliquer sur Fin pour quitter l'assistant."), -1, (20, 145, 148, 9), 1342177280], [130, _("Pour des exemples de bons messages, vous pouvez utiliser votre 'Bo\xeete de r\xe9ception' mais vous evez \xeatre S\xdbR qu'elle ne contient aucun message non sollicit\xe9"), -1, (20, 42, 247, 26), 1342177280], [130, _('Si faire ce tri \xe9tait trop fastidieux, cr\xe9ez simplement un dossier temporaire en mettant quelques messages en exemple.'), -1, (20, 58, 247, 17), 1342177280], [130, _("Pour des exemples de messages non sollicit\xe9s vous pouvez utiliser le dossier 'El\xe9ments supprim\xe9s'. Si faire ce tri \xe9tait trop fastidieux, cr\xe9ez simplement un dossier temporaire en mettant quelques messages en exemple."), -1, (20, 80, 247, 35), 1342177280], [130, _("Lorsque vous aurez termin\xe9, ouvrez le SpamBayes Manager via la barre d'outil SpamBayes, et red\xe9marrez l'assistant."), -1, (20, 121, 245, 17), 1342177280]], 'IDD_FOLDER_SELECTOR': [[_('Dialog'), (0, 0, 253, 215), -1865940800, None, (8, 'Tahoma')], [130, _('&Dossiers :'), -1, (7, 7, 47, 9), 1342177280], ['SysTreeView32', '', 1040, (7, 21, 172, 140), 1350631735], [128, _('(sub)'), 1041, (7, 167, 126, 9), 1342242819], [130, _('(status1)'), 1043, (7, 180, 220, 9), 1342177280], [130, _('(status2)'), 1044, (7, 194, 220, 9), 1342177280], [128, _('OK'), 1, (190, 21, 57, 14), 1342177281], [128, _('Annuler'), 2, (190, 39, 57, 14), 1342177280], [128, _('&Tout effacer'), 1042, (190, 58, 57, 14), 1342177280], [128, _('&Nouveau dossier'), 1046, (190, 77, 58, 14), 1342177280]], 'IDD_STATISTICS': [[_('Statistiques'), (0, 0, 248, 257), 1354760256, None, (8, 'Tahoma')], [128, _('Statistiques'), -1, (7, 3, 241, 229), 1342177287], [130, _('some stats\\nand some more\\nline 3\\nline 4\\nline 5'), 1095, (12, 12, 230, 204), 1342177280], [128, _('Remise \xe0 0 des statistiques'), 1096, (156, 238, 92, 14), 1342177280], [130, _('Derni\xe8re remise \xe0 0 :'), -1, (7, 241, 36, 8), 1342177280], [130, _('<<>>'), 1097, (47, 241, 107, 8), 1342177280]], 'IDD_ADVANCED': [[_('Avanc\xe9'), (0, 0, 248, 257), 1355284672, 1024, (8, 'Tahoma')], [128, _('D\xe9lais de filtrage'), -1, (7, 3, 234, 117), 1342177287], ['msctls_trackbar32', '', 1056, (16, 36, 148, 22), 1342242821], [130, _('D\xe9lai avant filtrage'), -1, (16, 26, 101, 8), 1342177280], [129, '', 1057, (165, 39, 40, 14), 1350566016], [130, _('secondes'), -1, (208, 41, 28, 8), 1342177280], ['msctls_trackbar32', '', 1058, (16, 73, 148, 22), 1342242821], [130, _('D\xe9lai de filtrage entre deux messages'), -1, (16, 62, 142, 8), 1342177280], [129, '', 1059, (165, 79, 40, 14), 1350566016], [130, _('secondes'), -1, (207, 82, 28, 8), 1342177280], [128, _('Seulement pour les dossiers qui re\xe7oivent de nouveaux messages'), 1060, (16, 100, 217, 10), 1342242819], [128, _('Afficher le r\xe9pertoire de donn\xe9es'), 1071, (7, 238, 111, 14), 1342177280], [128, _('Activer le filtrage en t\xe2che de fond'), 1091, (16, 12, 162, 10), 1342242819], [128, _('Diagnostiques...'), 1080, (171, 238, 70, 14), 1342177280]], 'IDD_WIZARD_FINISHED_UNCONFIGURED': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('Annulation du param\xe9trage'), -1, (20, 4, 247, 14), 1342177280], [130, _("L'\xe9cran principal de SpamBayes va maintenant \xeatre affich\xe9. Vous devez d\xe9finir les dossiers et activer SpamBayes pour commencer \xe0 filtrer les messages."), -1, (20, 29, 247, 16), 1342177280], [130, _("Cliquer sur Fin pour quitter l'assistant."), -1, (20, 139, 148, 9), 1342177280]], 'IDD_WIZARD_WELCOME': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _("Bienvenue dans l'assistant de param\xe9trage de SpamBayes"), -1, (20, 4, 191, 14), 1342177280], [130, _('Cet assistant va vous guider dans le param\xe9trage du module SpamBayes pour Outlook. Merci de pr\xe9ciser o\xf9 vous en \xeates pour le param\xe9trage.'), -1, (20, 20, 255, 18), 1342177280], [128, _("Je n'ai rien pr\xe9par\xe9 du tout pour SpamBayes."), 1081, (20, 42, 190, 11), 1342309385], [128, _("J'ai d\xe9j\xe0 filtr\xe9 les bon messages (ham) et les mauvais (spam) dans des dossiers s\xe9par\xe9s adapt\xe9s \xe0 l'entra\xeenement."), -1, (20, 59, 255, 18), 1342186505], [128, _('Je pr\xe9f\xe8re me d\xe9brouiller tout seul pour configurer SpamBayes.'), -1, (20, 82, 187, 12), 1342178313], [130, _("Pour plus d'informations sur l'entra\xeenement et le param\xe9trage de SpamBayes, cliquer sur le bouton A propos."), -1, (20, 103, 185, 26), 1342177280], [128, _('A propos...'), 1017, (215, 104, 60, 15), 1342177280], [130, _("Si vous quittez l'assistant, vous pouvez le relancer \xe0 partir du SpamBayes Manager, disponible sur la barre d'outil SpamBayes."), -1, (20, 137, 232, 17), 1342177280]], 'IDD_FILTER_NOW': [[_('Filtrer maintenant'), (0, 0, 244, 185), -1865940928, 1024, (8, 'Tahoma')], [130, _('Filtrer les dossiers suivants'), -1, (8, 9, 168, 11), 1342177280], [130, _('Dossiers...\\nLine 2'), 1036, (7, 20, 172, 12), 1342181900], [128, _('Parcourir...'), 1037, (187, 19, 50, 14), 1342177280], [128, _('Filtres et actions'), -1, (7, 38, 230, 40), 1342308359], [128, _('Effectuer les actions (aiguillage du message)'), 1019, (15, 49, 126, 10), 1342373897], [128, _("Attribuer une note mais ne pas effectuer d'action"), 1018, (15, 62, 203, 10), 1342177289], [128, _('Restrendre le filtre'), -1, (7, 84, 230, 35), 1342308359], [128, _('Aux messages non lus'), 1020, (15, 94, 149, 9), 1342242819], [128, _("Aux messages qui n'ont pas eu de note attribu\xe9e"), 1021, (15, 106, 149, 9), 1342242819], ['msctls_progress32', _('Progress1'), 1000, (7, 129, 230, 11), 1350565888], [130, _('Static'), 1001, (7, 144, 227, 10), 1342177280], [128, _('D\xe9marrer le filtrage'), 1006, (7, 161, 67, 14), 1342177281], [128, _('Fermer'), 2, (187, 162, 50, 14), 1342177280]], 'IDD_WIZARD_TRAINING_IS_IMPORTANT': [['', (0, 0, 328, 156), 1354760384, 1024, (8, 'Tahoma')], [130, _("SpamBayes ne fonctionnera pas tant qu'il ne s'est pas entra\xeen\xe9."), -1, (11, 8, 312, 14), 1342177280], [128, _("A propos de l'entra\xeenement..."), 1017, (258, 135, 65, 15), 1342177280], [130, _("SpamBayes est un syst\xe8me qui apprend \xe0 reconna\xeetre les bons et les mauvais messages \xe0 partir des exemples que vous lui donnez. A la base, il ne dispose d'aucun filtres, il doit donc \xeatre entra\xeen\xe9 pour devenir effectif."), -1, (11, 21, 312, 30), 1342177280], [130, _("Pour commencer, SpamBayes va aiguiller tous vos messages dans le dossier 'Douteux'. L'entra\xeenement est simple : pour chaque message, vous sp\xe9cifiez alors s'il s'agit de spam ou non \xe0 partir des boutons 'C'est du spam' et 'Ce n'est pas du spam'. Petit \xe0 pete????ctls_progress32"), -1, (22, 61, 301, 35), 1342177280], [130, _("Cette option fermera l'assistant et vous dire comment aiguiller vos messages. Vous pourrez param\xe9trer SpamBayes et le rendre actif imm\xe9diatement sur vos messages"), -1, (22, 113, 301, 27), 1342177280], [130, _("Pour plus d'information, cliquer sur le bouton A propos de l'entra\xeenement."), -1, (11, 137, 234, 8), 1342177280], [128, _("Je veux stopper l'entra\xeenement et laisser SpamBayes apprendre sur les nouveaux messages"), 1088, (11, 50, 312, 11), 1342308361], [128, _('Je vais effectuer le pr\xe9-tri moi-m\xeame (bon / spam) et param\xe9trer SpamBayes plus tard'), 1089, (11, 98, 312, 11), 1342177289]], 'IDD_FILTER_UNSURE': [[_('Messages douteux'), (0, 0, 249, 124), 1355284672, None, (8, 'Tahoma')], [130, _("Pour \xeatre consid\xe9r\xe9 comme douteux, un message doit obtenir une note d'au moins"), -1, (12, 11, 212, 10), 1342177280], ['msctls_trackbar32', '', 1029, (12, 18, 165, 20), 1342242821], [129, '', 1030, (183, 24, 54, 14), 1350566016], [130, _('et ces messages doivent \xeatre :'), -1, (12, 38, 107, 10), 1342177280], [133, '', 1031, (12, 49, 55, 40), 1344339971], [130, _('dans le dossier'), -1, (74, 52, 31, 10), 1342177280], [130, _('(folder name)'), 1033, (119, 49, 59, 14), 1342312972], [128, _('Pa&rcourir'), 1034, (183, 49, 50, 14), 1342177280], [128, _('Marquer les messages l&us'), 1051, (12, 70, 101, 10), 1342242819]], 'IDD_WIZARD_FINISHED_UNTRAINED': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [130, _('Bravo !'), -1, (20, 4, 247, 14), 1342177280], [130, _('SpamBayes est maintenant param\xe9tr\xe9 et pr\xeat \xe0 filtrer sur vos messages'), -1, (20, 22, 247, 16), 1342177280], [130, _("Comme SpamBayes ne s'est pas encore entra\xeen\xe9, tous les messages vont \xeatre rang\xe9s dans le dossier Douteux (Unsure). Pour chacun des messages, vous devez cliquer soit sur 'C'est du Spam' soit sur 'Ce n'est pas du Spam'."), -1, (20, 42, 247, 27), 1342177280], [130, _("Pour acc\xe9l\xe9rer l'entra\xeenement, vous pouvez d\xe9placer manuellement tous les spams de votre 'Bo\xeete de r\xe9ception' dans le dossier 'Spam', et alors s\xe9lectionner 'Entra\xeenement' depuis le SpamBayes manager."), -1, (20, 83, 247, 31), 1342177280], [130, _("Plus le programme s'entra\xeene et plus la fiabilit\xe9 augmente. Notez qu'apr\xe8s seulement quelques messages le r\xe9sultat est \xe9tonnant."), -1, (20, 69, 247, 15), 1342177280], [130, _("Cliquer sur Fin pour sortir de l'assistant."), -1, (20, 121, 148, 9), 1342177280]], 'IDD_GENERAL': [[_('General'), (0, 0, 253, 257), 1355284672, 1024, (8, 'Tahoma')], [130, _('SpamBayes Version'), 1009, (6, 54, 242, 8), 1342177280], [130, _("SpamBayes a besoin de s'entra\xeener avant d'\xeatre activ\xe9. Cliquer sur l'onglet 'Entra\xeenement', ou utilisez l'assistant en vous laissant guider."), -1, (6, 67, 242, 17), 1342177280], [130, _("Status de la base d'entra\xeenement :"), -1, (6, 90, 222, 8), 1342177280], [130, _('123 spams ; 456 bons messages\\r\\nLine2\\r\\nLine3'), 1035, (6, 101, 242, 27), 1342181376], [128, _('Activer SpamBayes'), 1013, (6, 221, 97, 11), 1342242819], [130, _('Les messages classifi\xe9s comme spam sont aiguill\xe9s dans le dossier Folder1\\nLes messages douteux sont \xe9galement aiguill\xe9s'), 1014, (6, 146, 242, 67), 1342181376], [128, _('Revenir au param\xe9trage initial...'), 1073, (6, 238, 109, 14), 1342177280], [128, _('Assistant...'), 1070, (142, 238, 106, 15), 1342177280], [130, _('Status des filtres :'), -1, (6, 135, 222, 8), 1342177280], [130, '1062', 1063, (0, 2, 275, 52), 1342179342]], 'IDD_FILTER': [[_('Filtrage'), (0, 0, 249, 257), 1355284672, 1024, (8, 'Tahoma')], [130, _('Filtrer les dossiers suivant lors de la r\xe9ception de nouveaux messages'), -1, (8, 4, 168, 11), 1342177280], [130, _('Dossiers...\\nLine 2'), 1038, (7, 16, 177, 12), 1342312972], [128, _('Parcourir...'), 1039, (192, 14, 50, 14), 1342177280], [128, _('Spam s\xfbr'), -1, (7, 33, 235, 80), 1342177287], [130, _("Pour \xeatre consid\xe9r\xe9 comme un spam, un message doit obtenir une note d'au moins"), -1, (13, 42, 212, 10), 1342177280], ['msctls_trackbar32', _('Slider1'), 1023, (13, 52, 165, 22), 1342242821], [129, '', 1024, (184, 53, 51, 14), 1350566016], [130, _('et ces messages doivent \xeatre :'), -1, (13, 72, 107, 10), 1342177280], [133, '', 1025, (12, 83, 55, 40), 1344339971], [130, _('dans le dossier'), -1, (71, 85, 28, 10), 1342177280], [130, _('Folder names...'), 1027, (102, 83, 77, 14), 1342312972], [128, _('&Parcourir'), 1028, (184, 83, 50, 14), 1342177280], [128, _('Message douteux'), -1, (6, 117, 235, 81), 1342177287], [130, _("Pour \xeatre consid\xe9r\xe9 comme douteux, un message doit obtenir une note d'au moins"), -1, (12, 128, 212, 10), 1342177280], ['msctls_trackbar32', _('Slider1'), 1029, (12, 135, 165, 20), 1342242821], [129, '', 1030, (183, 141, 54, 14), 1350566016], [130, _('et ces messages doivent \xeatre :'), -1, (12, 155, 107, 10), 1342177280], [133, '', 1031, (12, 166, 55, 40), 1344339971], [130, _('dans le dossier'), -1, (71, 169, 48, 8), 1342177280], [130, _('(folder name)'), 1033, (122, 166, 57, 14), 1342312972], [128, _('P&arcourir'), 1034, (184, 166, 50, 14), 1342177280], [128, _('Marquer les spams comme lus'), 1047, (13, 100, 81, 10), 1342242819], [128, _('Marquer les messages douteux comme lus'), 1051, (12, 186, 101, 10), 1342242819], [128, _('Bons messages'), -1, (6, 203, 235, 48), 1342177287], [130, _('Ces messages doivent \xeatre :'), -1, (12, 215, 107, 10), 1342177280], [133, '', 1032, (12, 228, 55, 40), 1344339971], [130, _('dans le dossier'), -1, (71, 230, 48, 8), 1342177280], [130, _('(folder name)'), 1083, (122, 228, 57, 14), 1342312972], [128, _('Pa&rcourir...'), 1004, (184, 228, 50, 14), 1342177280]], 'IDD_NOTIFICATIONS': [[_('Notifications'), (0, 0, 248, 257), 1354760256, None, (8, 'Tahoma')], [128, _('New Mail Sounds'), -1, (7, 3, 241, 229), 1342177287], [128, _('Enable new mail notification sounds'), 1098, (14, 17, 129, 10), 1342242819], [130, _('Good sound:'), -1, (14, 31, 42, 8), 1342177280], [129, '', 1094, (14, 40, 174, 14), 1350566016], [128, _('Browse...'), 1101, (192, 40, 50, 14), 1342177280], [130, _('Unsure sound:'), -1, (14, 58, 48, 8), 1342177280], [129, '', 1095, (14, 67, 174, 14), 1350566016], [128, _('Browse...'), 1102, (192, 67, 50, 14), 1342177280], [130, _('Spam sound:'), -1, (14, 85, 42, 8), 1342177280], [129, '', 1096, (14, 94, 174, 14), 1350566016], [128, _('Browse...'), 1103, (192, 94, 50, 14), 1342177280], [130, _('Time to wait for additional messages:'), -1, (14, 116, 142, 8), 1342177280], ['msctls_trackbar32', '', 1099, (14, 127, 148, 22), 1342242821], [129, '', 1100, (163, 133, 40, 14), 1350566016], [130, _('seconds'), -1, (205, 136, 28, 8), 1342177280]], 'IDD_WIZARD_FOLDERS_REST': [['', (0, 0, 284, 162), 1354760384, 1024, (8, 'Tahoma')], [128, _('Parcourir...'), 1005, (208, 85, 60, 15), 1342177280], [130, _('Dossiers Spam et Douteux'), -1, (20, 4, 247, 14), 1342177280], [130, _("SpamBayes utilise deux dossiers pour g\xe9rer le spam. Un dossier 's\xfbr' pour stocker le spam et un dossier 'douteux' qu'il vous faudra aiguiller manuellement."), -1, (20, 20, 247, 22), 1342177280], [130, _("Si vous entrez un nom de dossier qui n'existe pas, il va \xeatre cr\xe9\xe9 automatiquement. Pour choisir un dossier existant, cliquer sur Parcourir."), -1, (20, 44, 243, 24), 1342177280], [129, '', 1027, (20, 85, 179, 14), 1350566016], [130, _('Les messages douteux vont \xeatre rang\xe9s dans le dossier nomm\xe9'), -1, (20, 105, 186, 12), 1342177280], [129, '', 1033, (20, 117, 177, 14), 1350566016], [130, _('Les messages spam vont \xeatre rang\xe9s dans le dossier nomm\xe9'), -1, (20, 72, 137, 8), 1342177280], [128, _('Parcourir...'), 1034, (208, 117, 60, 15), 1342177280]]} ids = {'IDC_DELAY1_SLIDER': 1056, 'IDC_PROGRESS': 1000, 'IDD_MANAGER': 101, 'IDD_DIAGNOSTIC': 113, 'IDD_TRAINING': 102, 'IDC_DELAY2_TEXT': 1059, 'IDC_DELAY1_TEXT': 1057, 'IDD_WIZARD': 114, 'IDC_BROWSE_SPAM_SOUND': 1103, 'IDC_STATIC_HAM': 1002, 'IDC_PROGRESS_TEXT': 1001, 'IDD_GENERAL': 108, 'IDC_BROWSE_UNSURE_SOUND': 1102, 'IDC_TAB': 1068, 'IDC_FOLDER_UNSURE': 1033, 'IDC_VERBOSE_LOG': 1061, 'IDC_EDIT1': 1094, 'IDC_BROWSE': 1037, 'IDC_BACK_BTN': 1069, 'IDD_WIZARD_FINISHED_UNCONFIGURED': 119, 'IDC_ACTION_CERTAIN': 1025, 'IDC_BUT_ACT_ALL': 1019, 'IDD_FILTER_NOW': 104, 'IDC_BROWSE_HAM_SOUND': 1101, 'IDC_MARK_SPAM_AS_READ': 1047, 'IDC_RECOVER_RS': 1075, 'IDC_STATIC': -1, 'IDC_PAGE_PLACEHOLDER': 1078, 'IDC_BROWSE_WATCH': 1039, 'IDC_ACCUMULATE_DELAY_TEXT': 1100, 'IDC_FOLDER_HAM': 1083, 'IDD_WIZARD_FOLDERS_REST': 117, 'IDC_SHOW_DATA_FOLDER': 1071, 'IDC_BUT_ACT_SCORE': 1018, '_APS_NEXT_RESOURCE_VALUE': 129, '_APS_NEXT_SYMED_VALUE': 101, 'IDC_SLIDER_CERTAIN': 1023, 'IDC_BUT_UNREAD': 1020, 'IDC_BUT_ABOUT': 1017, 'IDC_BUT_RESCORE': 1008, 'IDC_BUT_SEARCHSUB': 1041, 'IDC_BUT_TRAIN_FROM_SPAM_FOLDER': 1010, 'IDC_LAST_RESET_DATE': 1097, 'IDD_WIZARD_FOLDERS_TRAIN': 120, 'IDC_BUT_FILTER_ENABLE': 1013, 'IDC_ABOUT_BTN': 1072, 'IDD_WIZARD_FINISHED_TRAINED': 122, 'IDD_FOLDER_SELECTOR': 105, 'IDD_STATISTICS': 107, 'IDC_LIST_FOLDERS': 1040, 'IDB_SBWIZLOGO': 125, 'IDC_BUT_VIEW_LOG': 1093, 'IDC_STATUS2': 1044, 'IDC_STATUS1': 1043, 'IDCANCEL': 2, 'IDC_BROWSE_HAM': 1004, 'IDC_BROWSE_SPAM': 1005, 'IDD_WIZARD_FINISHED_UNTRAINED': 116, 'IDC_MARK_UNSURE_AS_READ': 1051, 'IDC_BROWSE_HAM_SOUND2': 1103, 'IDC_BUT_WIZARD': 1070, 'IDC_VERSION': 1009, 'IDC_FOLDER_NAMES': 1036, 'IDC_BUT_TIMER_ENABLED': 1091, 'IDC_SLIDER_UNSURE': 1029, 'IDC_BUT_NEW': 1046, 'IDC_FOLDER_WATCH': 1038, 'IDC_BUT_UNTRAINED': 1088, 'IDC_STATIC_SPAM': 1003, 'IDC_EDIT_UNSURE': 1030, 'IDC_BUT_CLEARALL': 1042, 'IDC_BUT_UNSEEN': 1021, 'IDD_WIZARD_FOLDERS_WATCH': 118, 'IDC_HAM_SOUND': 1094, 'IDC_EDIT_CERTAIN': 1024, 'IDC_BUT_FILTER_DEFINE': 1016, 'IDC_FORWARD_BTN': 1077, '_APS_NEXT_CONTROL_VALUE': 1102, 'IDC_INBOX_TIMER_ONLY': 1060, 'IDD_ADVANCED': 106, 'IDC_WIZ_GRAPHIC': 1092, 'IDD_FILTER_UNSURE': 40002, 'IDC_DEL_SPAM_RS': 1074, 'IDB_FOLDERS': 127, 'IDC_BUT_PREPARATION': 1081, 'IDC_DELAY2_SLIDER': 1058, 'IDC_ACCUMULATE_DELAY_SLIDER': 1099, 'IDC_SAVE_SPAM_SCORE': 1048, 'IDC_FOLDER_CERTAIN': 1027, 'IDB_SBLOGO': 1062, 'IDC_BROWSE_UNSURE': 1034, 'IDC_STATISTICS': 1095, 'IDC_BUT_RESET_STATS': 1096, 'IDC_BUT_TRAIN_TO_SPAM_FOLDER': 1011, 'IDD_FILTER_SPAM': 110, 'IDC_BUT_RESET': 1073, 'IDD_NOTIFICATIONS': 128, 'IDC_ACTION_UNSURE': 1031, 'IDD_WIZARD_TRAIN': 121, 'IDD_WIZARD_FINISHED_TRAIN_LATER': 124, 'IDC_ACTION_HAM': 1032, 'IDC_BUT_REBUILD': 1007, '_APS_NEXT_COMMAND_VALUE': 40001, 'IDC_ENABLE_SOUNDS': 1098, 'IDC_SPAM_SOUND': 1096, 'IDC_UNSURE_SOUND': 1095, 'IDD_WIZARD_TRAINING_IS_IMPORTANT': 123, 'IDC_TRAINING_STATUS': 1035, 'IDD_WIZARD_WELCOME': 115, 'IDC_BUT_TRAIN': 1089, 'IDC_START': 1006, 'IDD_FILTER': 103, 'IDC_LOGO_GRAPHIC': 1063, 'IDC_FILTER_STATUS': 1014, 'IDOK': 1, 'IDC_BROWSE_CERTAIN': 1028, 'IDC_BUT_SHOW_DIAGNOSTICS': 1080, 'IDC_BUT_TRAIN_NOW': 1012} names = {1024: 'IDC_EDIT_CERTAIN', 1: 'IDOK', 2: 'IDCANCEL', 1027: 'IDC_FOLDER_CERTAIN', 1028: 'IDC_BROWSE_CERTAIN', 1029: 'IDC_SLIDER_UNSURE', 1030: 'IDC_EDIT_UNSURE', 1031: 'IDC_ACTION_UNSURE', 1032: 'IDC_ACTION_HAM', 1033: 'IDC_FOLDER_UNSURE', 1034: 'IDC_BROWSE_UNSURE', 1035: 'IDC_TRAINING_STATUS', 1036: 'IDC_FOLDER_NAMES', 1037: 'IDC_BROWSE', 1038: 'IDC_FOLDER_WATCH', 1039: 'IDC_BROWSE_WATCH', 1040: 'IDC_LIST_FOLDERS', 1041: 'IDC_BUT_SEARCHSUB', 1042: 'IDC_BUT_CLEARALL', 1043: 'IDC_STATUS1', 1044: 'IDC_STATUS2', 1046: 'IDC_BUT_NEW', 1047: 'IDC_MARK_SPAM_AS_READ', 1048: 'IDC_SAVE_SPAM_SCORE', 1051: 'IDC_MARK_UNSURE_AS_READ', 1056: 'IDC_DELAY1_SLIDER', 1057: 'IDC_DELAY1_TEXT', 1058: 'IDC_DELAY2_SLIDER', 1059: 'IDC_DELAY2_TEXT', 1060: 'IDC_INBOX_TIMER_ONLY', 1061: 'IDC_VERBOSE_LOG', 1062: 'IDB_SBLOGO', 1063: 'IDC_LOGO_GRAPHIC', 1068: 'IDC_TAB', 1069: 'IDC_BACK_BTN', 1070: 'IDC_BUT_WIZARD', 1071: 'IDC_SHOW_DATA_FOLDER', 1072: 'IDC_ABOUT_BTN', 1073: 'IDC_BUT_RESET', 1074: 'IDC_DEL_SPAM_RS', 1075: 'IDC_RECOVER_RS', 1077: 'IDC_FORWARD_BTN', 1078: 'IDC_PAGE_PLACEHOLDER', 1080: 'IDC_BUT_SHOW_DIAGNOSTICS', 1081: 'IDC_BUT_PREPARATION', 1083: 'IDC_FOLDER_HAM', 1088: 'IDC_BUT_UNTRAINED', 1089: 'IDC_BUT_TRAIN', 40002: 'IDD_FILTER_UNSURE', 1091: 'IDC_BUT_TIMER_ENABLED', 1025: 'IDC_ACTION_CERTAIN', 1093: 'IDC_BUT_VIEW_LOG', 1094: 'IDC_EDIT1', 1095: 'IDC_STATISTICS', 1096: 'IDC_BUT_RESET_STATS', 1097: 'IDC_LAST_RESET_DATE', 1098: 'IDC_ENABLE_SOUNDS', 1099: 'IDC_ACCUMULATE_DELAY_SLIDER', 1100: 'IDC_ACCUMULATE_DELAY_TEXT', 1101: 'IDC_BROWSE_HAM_SOUND', 1102: 'IDC_BROWSE_UNSURE_SOUND', 1103: 'IDC_BROWSE_HAM_SOUND2', 101: 'IDD_MANAGER', 102: 'IDD_TRAINING', 103: 'IDD_FILTER', 104: 'IDD_FILTER_NOW', 105: 'IDD_FOLDER_SELECTOR', 106: 'IDD_ADVANCED', 107: 'IDD_STATISTICS', 108: 'IDD_GENERAL', 110: 'IDD_FILTER_SPAM', 113: 'IDD_DIAGNOSTIC', 114: 'IDD_WIZARD', 115: 'IDD_WIZARD_WELCOME', 116: 'IDD_WIZARD_FINISHED_UNTRAINED', 117: 'IDD_WIZARD_FOLDERS_REST', 118: 'IDD_WIZARD_FOLDERS_WATCH', 119: 'IDD_WIZARD_FINISHED_UNCONFIGURED', 120: 'IDD_WIZARD_FOLDERS_TRAIN', 121: 'IDD_WIZARD_TRAIN', 122: 'IDD_WIZARD_FINISHED_TRAINED', 123: 'IDD_WIZARD_TRAINING_IS_IMPORTANT', 124: 'IDD_WIZARD_FINISHED_TRAIN_LATER', 125: 'IDB_SBWIZLOGO', 127: 'IDB_FOLDERS', 128: 'IDD_NOTIFICATIONS', 129: '_APS_NEXT_RESOURCE_VALUE', 40001: '_APS_NEXT_COMMAND_VALUE', 1092: 'IDC_WIZ_GRAPHIC', 1000: 'IDC_PROGRESS', 1001: 'IDC_PROGRESS_TEXT', 1002: 'IDC_STATIC_HAM', 1003: 'IDC_STATIC_SPAM', 1004: 'IDC_BROWSE_HAM', 1005: 'IDC_BROWSE_SPAM', 1006: 'IDC_START', 1007: 'IDC_BUT_REBUILD', 1008: 'IDC_BUT_RESCORE', 1009: 'IDC_VERSION', 1010: 'IDC_BUT_TRAIN_FROM_SPAM_FOLDER', 1011: 'IDC_BUT_TRAIN_TO_SPAM_FOLDER', 1012: 'IDC_BUT_TRAIN_NOW', 1013: 'IDC_BUT_FILTER_ENABLE', 1014: 'IDC_FILTER_STATUS', 1016: 'IDC_BUT_FILTER_DEFINE', 1017: 'IDC_BUT_ABOUT', 1018: 'IDC_BUT_ACT_SCORE', 1019: 'IDC_BUT_ACT_ALL', 1020: 'IDC_BUT_UNREAD', 1021: 'IDC_BUT_UNSEEN', -1: 'IDC_STATIC', 1023: 'IDC_SLIDER_CERTAIN'} bitmaps = {'IDB_SBWIZLOGO': 'sbwizlogo.bmp', 'IDB_SBLOGO': 'sblogo.bmp', 'IDB_FOLDERS': 'folders.bmp'} def ParseDialogs(s): return FakeParser() spambayes-1.1a6/spambayes/languages/fr/i18n.ui.html0000775000076500000240000007710610646440126022324 0ustar skipstaff00000000000000 SpamBayes - Interface utilisateur

    Introduction

    Cette page, ui.html, dfinit l'aspect visuel de l'interface utilisateur du serveur SpamBayes. Les diffrentes parties de code HTML dfinies ici sont extraites et gnres l'excution pour produire du code HTML dynamique que le serveur SpamBayes va servir - ce fichier est un ensemble de de composants HTML. PyMeldLite est le fantastique module qui fournit la correspondance entre les objets et le code HTML. Chaque partie de code HTML rcrire est signal par des balises du type id, et devient un objet Python pendant la phase d'excution.

    Cette "Introduction" est une prsentation de ce fichier. Elle n'a pas besoin d'tre traduite et n'est jamais utilise dans l'interface homme-machine.

    Voici un exemple du mode de fonctionnement : une bote de dialogue avec un id de examplebox: PyMeldLite vous permet de manipuler le code HTML par programmation :

        >>> import PyMeldLite
        >>> html = open("ui.html", "rt").read()
        >>> doc = PyMeldLite.Container(html)
        >>> print doc.examplebox
        <input id="examplebox" size="10" type="text" value="exemple"/>
        >>> doc.examplebox.value = "Chang"
        >>> print doc.examplebox
        <input id="examplebox" size="10" type="text" value="Chang"/>
        

    Le code Python ncessaire la gnration de l'interface utilisateur HTML n'a pas besoin de s'embter concatner des chanes ou construire des composants HTML de zro dans le code. L'aspect visuel est dnini uniquement par ce fichier HTML - changement de feuille de style, traduction (autres langues), ajout d'une extension l'interface utilisateur - et tout cela trs simplement.

    Les composants de l'interface utilisateur figurent ci-dessous avec leurs ids.


    headedBox

    Headed box
      Ceci est le "headedBox". La plupart des lments de l'interface utilisateur est prsente dans des botes comme celle-ci. Les lements ne sont pas prsents ici ui.html pour viter une duplication de code HTML. Telle quelle, cette section n'a pas besoin d'tre traduite.
     

    Aide

    SpamBayes - Aide
      Navr, pas d'aide disponible pour cette section.

    Si vous pensez avoir dcouvert un bogue (bug) dans SpamBayes, ou que vous tes perdu dans la manire de procder (installation / mise en oeuvre, ...), vous pouvez vous adresser la mailing list pour obtenir de l'aide. Veuillez noter que les membres de cette liste sont des volontaires qui rpondent sur leur temps libre. Une rponse vos questions peut prendre un certain temps.
     
    Si vous tiez quasiment sr d'avoir trouv un bogue, le mieux est de le soumettre via le Suivi SourceForge, ceci vitera d'ventuelles pertes de messages dans la possible masse de messages adresss la liste et palier aux dsagrment occasionns par des virus qui peuvent remplir la bote de messagerie.
     
    Lors de la soumission d'un message, merci d'tre aussi prcis que possible pour viter des changes inutiles tels que "merci de nous indiquer ceci ou cela". Il est de bonne augure d'inclure le mode opratoire nous permettant de reproduire le problme, le contenu des messages complet ayant provoqu le boque, une copie du message, ... ainsi que votre suggestion. Toute suggestion de traduction est galement la bienvenue. Pour vous aider, SpamBayes peut crer un message de demande d'assistance pour vous.

    Aide - Rsum des fonctionnalits

    Rsum des fonctionnalits

    Lorsque vous commencez utiliser SpamBayes, tous vos mails seront considrs comme 'Douteux' car SpamBayes n'a aucun moyen de reconnatre ce qui pour vous constitue un bon ou un mauvais message. Il va donc tre ncessaire de lui apprendre reconnatre les messages. Au fur et mesure, de moins en moins de messages seront considrs comme douteux et il ne restera plus que deux catgories de messages, les bons et les mauvais. Rien que lui montrer une vingtaine d'exemple de chaque est suffisant pour obtenir de bons rsultats. A un certain stades, vous constaterez mme que certains messages envoys automatiquement par des virus par exemple sont aiguills vers le dossier rserv aux messages non sollicits.

    SpamBayes conserve une copie temporaire de tous vos messages entrants, pour que vous soyez en mesure d'utiliser n'importe quel client de messagerie. Pour chacun de ces messages, vous indiquerez SpamBayes comment il convient de le considrer. La page affiche la liste des messages qui sont arrivs les %(cache_expiry_days) derniers jours et pour lesquels vous n'avez pas tabli de classification. Pour chaque message, vous devez choisir soit de l'ignorer (pas d'apprentissage sur ce message), d'attendre (garder le message pour un apprentissage futur), ou l'utiliser pour entraner SpamBayes (soit en tant que bon message - bon (ham), ou mauvais - spam). Pour cela, il suffit de simplement cliquer sur le cercle idoine. Pour aller plus vite, vous pouvez aussi cliquer sur le titre de la colonne pour classifier tous les messages d'un seul coup.

    Pour vous aider dterminer la nature du message, le sujet ainsi que l'emetteur du message vous sont prsents. Bien videmment, ces informations n'tant pas toujours suffisantes pour prendre votre dcision, vous pouvez galement en visualiser le contenu (en texte brut par scurit pour viter qu'un virus n'endommage votre systme) en cliquant sur le sujet du message.

    Une fois les actions choisies sur chacun des messages, il vous suffit de cliquer sur le bouton Apprentissage figurant en bas de page. SpamBayes mettra alors jour sa base de donne pour intgrer votre classification et en tiendra compte sur vos prochains messages.

    SpamBayes effectue cette classification sur vos nouveaux messages en fonction de vos choix prcdents. Si elle est correcte, vous pourrez choisir d'ignorer le message - c.f. le wiki SpamBayes pour une discussion sur les techniques d'apprentissage (en anglais). Vous pouvez galement consulter les lments (Tokens) contenus dans le message (pas uniquement les mots mais aussi d'autres lments gnrs par SpamBayes) et les indices (Clues) utiliss par SpamBayes pour classifier le message (notez que tous les les lments du messages ne sont pas utiliss pour la classification).

    Pour des soucis de visibilit, les nouveaux messages en attente de cette classification sont groups par leur date d'arrive. Des boutons Jour prcdent et Jour suivant sont votre disposition en haut de page pour changer de jour. Si un nouveau message arrive pendant que vous effectuez la classification, il ne sera pas automatiquement ajout la liste affiche l'cran ; il vous faudra cliquer sur le bouton Raffrachir en haut de page pour le voir apparatre.

    Aide - Statistiques

    SpamBayes conserve certaines informations sur la classification des messages. Cette page permet d'afficher les statistiques sur la classification des messages et l'tat actuel de l'apprentissage.

    Au jour d'aujourd'hui, la page affiche le nombre de messages considrs comme bon, mauvais (spam) ou douteux, le nombre de faux ngatifs et faux positifs et enfin combien de messages ont t considrs comme douteux (et comment vous les avez classifi).

    Notez que les donnes de cette page figurent dans la base de donnes "message info" utilise par SpamBayes depuis la dernire cration de la base (vous pouvez recrer la base sur demande).

    Aide - Page principale

    Ceci est la page principale d'aide sur l'interface Web de SpamBayes. Vous y trouverez l'tat actuel de SpamBayes ainsi que les liens vous permettant d'accder vos messages ou de modifier votre configuration.

    Cette page vous permet galement de pratiquer l'apprentissage initial soit partir de messages stocks dans des fichiers mbox (Unix) ou dbx (Outlook Express), soit partir d'un message que vous fournissez. Cliquez sur le bouton "Parcourir..." (ou collez le texte, en incluant les en-ttes), et cliquez sur le bouton appropri, soit Ceci est un bon message soit Ceci est du Spam

    De mme, si vous avez un message que vous souhaitez soumettre pour analyse, vous avez une fentre votre disposition. Deux solutions s'offrent vous, un copier/coller ou "Importer..." le message. Il suffira alors de cliquer sur le bouton Analyser et une page affichant comment SpamBayes a classifi le message s'affichera.

    Pour obtenir des informations sur un mot dans la base de donnes ddie aux statistiques (qui est le coeur de SpamBayes), vous pouvez utiliser le champ "Mot analyser". Entrez alors simplement le mot rechercher et cliquez sur le bouton Analyser ce mot. La recherche avance vous permet d'aller plus loin car elle admet les caratres gnriques et le expressions rgulires.

    Vous avez galement la possibilit d'obtenir des informations sur un message en partculier grce la copie temporaire que le systme conserve avant de vous les dlivrer. Ceci peut tre intressant si vous avez fait une erreur sur la classification d'un message et voulez repratiquer l'analyse. La recherche est poissible sur l'ensemble du message, que ce soit sur le sujet, les en-ttes, le corps du message ou encore les identifiants (ID) SpamBayes. Les messages correspondants sont affiches dans l'interface traditionnelle. Attention cependant, les messages qui ont expir (les messages ont une dure de vie de %(cache_expiry_days) jours) ne peuvent plus tre trouvs.


    Demande pr-remplie d'aide / soumission de bogue

    Send Help Message
    Emetteur :
    Sujet :
    Message :
    Fichier de trace joindre :

    Status

    Le mandataire (proxy) POP3 est en coute sur le port 1110, et relaie les donnes du serveur d'origine POP3 example.com.
    Connexions POP3 en cours : 0.
    Total de connexions POP3 pour cette session : 0.
    Rpartition des messages analyss durant cette sessions : Spam : 0, Bons : 0, Douteux : 0.
    Rpartition des messages utiliss pour l'apprentissage : Spam : 0 Bons : 0
    Statistiques complmentaires...
              Vous pouvez configurer SpamBayes
           partir de la page de paramtrage.
    Attention : mettez votre message d'alerte ici ! Ces alertes sont insres dynamiquement, la traduction n'est de fait pas ncessaire.

    reviewText

    Le proxy SpamBayes stocke tous les messages qu'il voit. Vous pouvez utiliser l'apprentissage sur ces messages partir de la page de Classification des messages.


    reviewTable

    Ici sont reprsents les messages que vous pouvez utiliser pour l'apprentissage. Appuyez sur le bouton appropri pour chaque message et clqiuer sur le bouton 'Apprentissage' ci-dessous. 'Mettre en attente' conserve le message ici pour remettre l'opration plus tard. Vous pouvez galement cliquer sur l'en-tte de colonne Annuler / Mettre en attente / Bon / Spam pour traiter tous les messages d'un coup. L'autre en-tte vous permet de trier les messages par type (attention, vous perdriez alors toute modification non valide faite sur la page).

              
     
    Messages reconnus comme tant du TYPE :
    Emetteur Sujet du message Reception le Annuler / En attente / Bon / Spam Score
    Richie Hindle <richie@entrian.com> Sat, 11 Sep 2003 19:03:11                  0.00% Indices | Elments
       
        

    Import

    Vous pouvez importer un message , mbox (unix) ou dbx :
    ou coller tout un message (en incluant les en-ttes) ici :

    (Le formulaire d'import est utilis aussi bien pour l'apprentissage que pour la classification - les lments inutiles seront supprims l'excution)


    Recherche d'un mot


    Requte simple
    Requte avec caractres gnriques (*, ?)
    Requte partir d'expressions rgulires
    Ignorer la casse
    Nombre maximal de messages

    Recherche d'un message

    Chercher dans...
    Identificateur SpamBayes
    Sujet
    En-tte de message
    Corps du message
    Ignorer la casse
    Nombre maximal de messages

    Statistiques d'un mot

    Nombre de messages de type spam : 123.
    Nombre de bons messages : 456.
    Probabilit qu'un message contenant ce mot soit un spam : 0.789.
    Mot # Spam # Bon Probabil
    spambayes 123 436 .789

    Rsultat de classification

    Probabilit de spam (aprs) : 0.123. Probabilit de spam (avant) : 0.125.

    Voici la table des indices menant cette probabilit
    Mot Probabil Occurences dans un bon message Occurences dans un spam
    Mot exemple 0.123 1 2

    Retour lapage principale ou classification d'un autre

    (La feuille de classification est ici)

    Formulaire de paramtrage

    Cette page vous permet de changer le comportement de SpamBayes relatif au traitement de vos messages. Vos choix sont stocks dans /chemin/exemple.

    Element      

    (Rserv l'aide)

    Element
    Valeur de l'lment 
         

    (Rserv l'aide)

    Valeur actuelle :  (valeur)
    Valeur actuelle :  (valeur)
     
    Nom du dossier / rpertoire

    (Rserv l'aide)


    Vous quittez le systme

    Termin. Merci, bientt.


    spambayes-1.1a6/spambayes/languages/fr/i18n_ui_html.py0000775000076500000240000003426310646440126023112 0ustar skipstaff00000000000000# -*- coding: ISO-8859-1 -*- """Resource i18n_ui_html (from file i18n.ui.html)""" # written by resourcepackage: (1, 0, 0) source = 'i18n.ui.html' package = 'languages.fr_FR' import zlib data = zlib.decompress("x}r\033ɕ:]\016\035\020\000m-QY[#{bJ\000]\002US\017\ ~ڷ}sʺ\0047bն\0049'=O&Nz}!^y-~3\021\034Fl4\ z~o<\034\017y.B:Ke2\032:7'rпJoD\011\035\006)_3y\012q ^\ g2R*u\013Y*?\031ѓRR,ru>Tz}\032h\002\021ebZ\006iv\020Q/V:W\ \032\010@_e*\016\030oηa\017\000F%=\017IJ*ʃ\\ecb VQfŬ(i\ \026_\014 \034?\011k\014Dqb \026*YRGX|c1\026ߔr\012\\=|xKx\001\ ljNQd\030DtS\0365L]dk{\036\037g374\020 wp|\0018N$'\ Z]ȝGRS&p*Ӕ\006yUi\014#Ec\005#=\021+\031:>r\037\017\0225\003\0307fy\ 2[Օ(2\0171M\000fsӬ,eQ\033\005\020\"\013Qs\017\036k]s\021XsU\"C[\ \000BELv\031G\037a=\021:.\027 \007/\"\000l&I\017\">Gn\035\ 7h\0317\003L:(\014H\007s\020\022:&\031V\022u(kngM\0168d^j\004>\007:Ҡ`UK\ BR7&O\014#\011&H`TZ𦈉\007\032X?TJp\031$\031H-\001kIwt\016\000t\ Р%(E\012\026jf/Q\\eL˂F\002Zoߨ$~\015x\033\022\020xw\006ʢ$dkb\000\017\007\ Ur\0049\004,e\012\000rD\020GU\022/\022oC\006r\0109d'o6QAL\012P(lQx\ \000{\001sQzepbRt\002Fƥ\017BE.\027}\022,`zj'UW\002_D\003d@>\024\ 09E\022<\030\"_$@y\0328\022VEo \024}\015x!BQ\024FF7\025D2Hܥ:XB\ S\014e\021J-W\0112#\031@HjL9&\012K\026\022~9L\\\001\026WA$Ÿ\ fW19':]U%~H@\004\001+G\000T\012o\023@~\032\0341#={\035]\020Ӏ8\ \021ޙ\010\015:\036;&O\014dw/0&0\030 /}p2\021RE\000F`\031̴\020`V9\010&\002\ \033\023me`r?)Kdbd'a1$h@c\0006&o\024\031\014\004@:Nk#X\ \010C`4b[q\026gsC`m\003\035k\033\005\000\027G`5Zȿh#\020+.\004\ }1U8Ц+&{\031oR`TН\005B5`kE`\017\020tΏRNؙj\016z9\023{\002{\012\ \033*\007\014Sd!Z9\021p\034;@C\\;\0026/\027\032\032Qi[[\014\025<0\0353=0\ \020>\000n\026h\002Ϫ`\033\015ޕ\0158P\030?C[ūv`E3\"\023}YR'f\ aX2@nݠ\034ЧQ8!\014!\011D\0304\015t \032d\014\015Qk(z|3l8=\ D\015@hU\024q\013,䰼\000\\\025\034\027\037C1A66_j\006\012Zal\003/3\005\036\004P\036\ Q+@\021\012\003\036ҨMAInj\010D\03362P\0107\011RԩH\017)ݰ\026\002䟂S4\002\026g\021\ C_dAfR\026*Q\035\002:s(`F\024;\031>\022\006\0155b\002D\032Κ\024jډǭ\ T*K c[aR*q\026\001\027\006#?cu\016 {f 6\0158:z\ \025_u\031ǡ\024pbʠ$h\015a\031\016bQ\023\0333/r1\012\001S\000j+\014n꽶\ \021*[\037gś(*cJ1\036@gus?@'\031\020BA\015\031\"^\010٩Jz!7Qnb\ Ȟ\006,q$\021A%2UsPp?D>g\031,(\006!s\001Ȧ`\0324N\ 8a0%;\037EQ\004\020K'r\035,\"\030cuJ!\0203W\036D\033\032/\026UNP\020\001\002\032\ \000\016ŷ`u\002uV\005<\032\032S)P,\0202!\014*0\031#\011\ x4BW7n\016\007hϗZUW\034ͨzlѳ\012R\024gk-‰BKU\010\036#5\032\0041B\ ?a~\000{|Q\013WZ\027/b\006*k\022ݬ\001q$6%O\026$C\036`\ [`Ƴ,\022\033H$x\016\025\004\0033\011\033x2'U\003͢Hbl›6E_*ha\020Ccd\017l\005.\ 4#ZN\035ug]P$0\0179\024Y#\"M\022㈆\032ya4\013StT\011\ \025\03045\031\010\031TLY5dـ\006\006}N\010'\012#\014CNpK. \ \011ֆ\022Ri˒Eg\025f\016\004J^#\014x~}6\010)Ad>\007暀\033\011\001\002bS\ +l \"\027\021G/F7s\006MV)Ly<ּE\001S\017:\026q.D\002\017\ l\015\031`\025_4\012T<~‘np!uhU,o\017\"[\024rCw\024P\017\010\\x\027\\j\017\ &7x4iE'Uݫ<&\034\011ep\007d\004}QQy\022Cz!0\006BA\0238q\ +\0102W\014E`\030*YȮ\025}\"D(*b%wKY\006Ć3E=R\032[f6.k`\ \011AZ%\000VY䏊\022Oi%f\001h'Z\007D\002qf^?\012\004܈\031\010M'\030=\ U$c\013;\033\030oc:Oتr\024\037\\\033d\012h^\017N\012$0$Dcb\"p厳\0330\022k\023\ ڏyCxdc\021R\\%W',ڌ`>ɩʇF\0028!gX3Qn\004TK<\015\ N\003~o\026/\024.__Tƹ\021=\005^Ab\013\033L\001F\030o~jC=K=3!͟e\002\ -\034XWC@b0E\013\020]H\004%;\031P\020홬A\011\034\005\016n`H\010Aj4%V~\ \007;L#LS\023*|~|I߱\\V\036\001RC\036\034iFgG9prW\026r\020Qa< i7\ w [XA1@D8W\027P$)Bk9Uu2N\0319q$%\023\022`/\032p\ \031LEeeH\016bpx0\011Wl0#\006i,i\030a\032kk{\024\0370\031Od\ lmp\003\021\015gþ\024-\020\0301*'\035Lf\034XT\011ETq\032ƓYS~TC\003\ K]\030}\000F1!Fy\\9Xº\0115\003&ekQxH\ \003E\000\031\033}\016CztLfA\003\037pQ\001U\032\0019u\"8R)\0120\ d&\031h\012&3B\037/ ,V\011\031|\000\0350Q6 {tkk(t,\ Κ쉽R0q-ڒvq\023GUO\022R4-lV&`1Poq3Ξ\037\ \027Y}b%Fƹ\013\022%u\032\025JjW/\026\032\031u\022foy\ D\015^\0329kYSf\021q^̜T[m\022\031ʈ\032>5g\022P*dg3aVr$P9\021o\ cZ`P`s\011\0274:\003\031˛2~\024ʨ'\000\031bݚ^\021Z{Tee?\ \027W+,{Cׂ՗kmoE>5=##eYp\034\\Ј\036U\012\012&V\030\017zSX\ E\007%\002q#]{\020dR\0061gpdo\006}e\017BRMؚC\000ެ!c\012\026\ \013l{譃\017L\013\023R\000 #_gǘ80\003L\034P\025\003na'\037\0217)\0002\ _qc{\035|QՑ\025M9K\005Y\006jM_m%\021fzJV[V$^8%{+\016\ ՌB\032\032덢\032\015{=\010(Pͳ\012=\022*B.Wo`pjg53x̏^Q\ O\\mi\0266]G0'\ ut +vIʅB\0121(9~o\014[ArYۢ30*\012\024\022r΄S\011f\ \032Y2m\026!ٹ^l\"Ơa$\0079a~&4\007*\034hL6\004{|s\010us5\ g.M\021\006E+47ւ\021=!\"e2A6\035+o^f&(N\024\014n\ Rj\037\023EJJ`\032lp\035mn\\zn\026 U;[\027\032˰;\0356J̗t\032\024t\ En\000\\d__K'_\035\0347\022ry\001@Ps]%\000kS,\0150\004\013l\022ld\020\022d.IwcI\"4~\000Bxdwo\ [w{\022\023Z僃F];u5y\017'^B-0f\035\036c~scs\002\ ly!8g\035\025Ż\0056n]i}O \003m\021\036=6ˆ\031\00590`\0368\017UHy4H[Xg\ xc%^\017\031prY@s<9yCa\0002K\031?@v1jZh3!\ 䛋pjU]o_ی!QFە7\024\033.`׋a]hA\0008|L;\011\ 7\033R\031\006xK߃.#\\5J[iҮ \007מ\016\0201\013A`ٹ/Zpf\ x~k\010\002=I!P,5\024\003nkW\011.)\002\"#)\005xڮl\021%|/\033Y\ \005/=BDZJCNIB*V,p\033\000@\020Vu\016M|\016\032[ٰ!\ z\003\003D,\006YU$}\030\"vn\020Y\020C\000rK\014W\007X \010!\026+|;\ nZIB6\000ݣR:D-'6\014i~kWhtgϟ?U?,K&\ t:U\031-3Igo\027LYT>eģ\020ay\020Sq_soW?I1\ \023\011f\006\013/NWJ4\035l\003@\017C!\013ÑPT<@z\ \015\027/_T渀Sa.U~&\013}\001F\013\005A\014dyzh%h\031\ ~J²]|p.#e\023l]M﫩\015\032lHV\012n^\021k\013\031<\ w|Cdׄj㐐+\004.ERJDؖ𗗝×:م㽄9F+y\017,֕nt\ NekZIy6Z\0273֫a-ƶcH鵊/\026\026Tdyyj\005;\005 '\ Ѷ}Ҍ`\021vF\025f6!W8\020s]D\020a\011&&AS3\034c_\000\\\022\ gx\012gC8p_GWr\031.:\022AH\034]䠑q/\0023we?۞(Z\ L|\006#\0353kh\033f\004Q]i\014\027\017ʟvk<\023\032){'Tqn*~\ !ms:{ _oۨ2H\013TF:_/?Db&v 4\ ]\037\004\032{?\027\005H7|\"@T\017\001\036&^V.h\013x8\036y8\015&F\ ,V+5@?;g2\013\011e%[>Hd;\030g5|BšA\003\035\024g1\ \034\007vt.e\006{+~\004; z\027\014v\016șjd\022tFĩ(6bn:b:URj\021V\ \007Rf_\0132J\033Ma/5\014L#k5\032\022E=\036\035\027[-a1p՛!ېW\036\016{[\ R{\026y\005\020\026sX6.Xr\001n4t^\016oq\027p\ \033z+4kE\031d\036xyyl`ޥ'>JiFB[{\007mF\ \013\037\000^\031 ;W߹F?n\023J\001&|\014\004\036R\ M[\031B0M\003\036\027L[F\016\022PL\033\034sy\035\024VN\035Z\002Z=\ S\015ΟSpq\016^he!lB T@ABQu}sl4%rtRҔ=-)z\ qr;\013\007w\037\006E-׫ޙx+T3\006n+6'W/>i\ m6\017]\020\\_\013֯~؊u_Ւ\001?bet)\001S\035e4,Ha/_<3\ \014'{\030AVg#P\003ze>\035\"\002WQ;7\010Ӎ<`~\023ExVCd:\ a-l^8-\025\035ShLp\003y}ٜVx\003\021m.\017t-G}nxxK\\lut;\ X\022T%AR\032GV+*=\033~O_#ٶB([a\034jbe?k^40ж.(O:*\ s\036}8hK\036\027㦓>am$\030?{t\030u6@ax+EJ)#\010P:\0359b\ [\030zV\027=r\0273zv'߂h\033\021-?7Z\0216\007~/ȶ\0072Z\\\ ͑nڎ\036\014\0147vyP./t/r6\036̴\012%+\020^Cބ\025@\017h\0048pi5\ {g\016cd\016x߯+||;WgӻԶ δEC,}8e)ۼڛ\013jk\ E\020d6N&x$4+5\015\013^2\006\022vԖ0oz\015.$l6|3\022ڡBrž\ Ѹ\005ǰxbZ\0165\015,$\031\023\017$\037p\000y,K\"ZQ#{o\ Much϶\033_fYgΖX\007Sg&TNgoi\"vӳp<\036z2\"ۍ(\ t@\027\014]`\026n82Z/\\uA7xnn\027X\013u\020fj&\"\ ؎n!м;iܾ\035IM9.>|\003C}\004m%\"os5(\003{1]\014һ!B\035\ \003׫W\031NpUȵ6ʄuL񽾮\ bVo\027B\015\027|\037\004Y\025nU\013\031ߗ46\022Wc\011'o6$\027\\k+\ \037s\025Ç\0172i\016͡ŗCW\033f|\024RpS!F\007}[\ L914K\015\015~fL\"\027WJ\011\005-UPҼeUU첻jlV6\ |\007p\037f&\\08\002K+d5\0367Q\013ra}") ### end spambayes-1.1a6/spambayes/mboxutils.py0000664000076500000240000002237111355063622020255 0ustar skipstaff00000000000000#! /usr/bin/env python """Utilities for dealing with various types of mailboxes. This is mostly a wrapper around the various useful classes in the standard mailbox module, to do some intelligent guessing of the mailbox type given a mailbox argument. +foo -- MH mailbox +foo +foo,bar -- MH mailboxes +foo and +bar concatenated +ALL -- a shortcut for *all* MH mailboxes /foo/bar -- (existing file) a Unix-style mailbox /foo/bar/ -- (existing directory) a directory full of .txt and .lorien files /foo/bar/ -- (existing directory with a cur/ subdirectory) Maildir mailbox /foo/Mail/bar/ -- (existing directory with /Mail/ in its path) alternative way of spelling an MH mailbox """ from __future__ import generators import os import sys import glob import email import mailbox import email.Message import re import traceback class DirOfTxtFileMailbox: """Directory of files each assumed to contain an RFC-822 message. If the filename ends with ".emlx", assumes that the file is an RFC-822 message wrapped in Apple Mail's proprietory .emlx format. The emlx format is simply the length of the message (as a string on the first line, then the raw message text, then the contents of a plist (XML) file that contains data that Mail uses (subject, flags, sender, and so forth). We ignore this plist data). Subdirectories are traversed recursively. """ def __init__(self, dirname, factory): self.names = glob.glob(os.path.join(dirname, "*")) self.names.sort() self.factory = factory def __iter__(self): for name in self.names: if os.path.isdir(name): for mbox in DirOfTxtFileMailbox(name, self.factory): yield mbox elif os.path.splitext(name)[1] == ".emlx": f = open(name) length = int(f.readline().rstrip()) yield self.factory(f.read(length)) f.close() else: try: f = open(name) except IOError: continue yield self.factory(f) f.close() def full_messages(msgs): """A generator that transforms each message by calling its get_full_message() method. Used for IMAP messages since they don't really have their content by default. """ for x in msgs: yield x.get_full_message() def _cat(seqs): for seq in seqs: for item in seq: yield item def getmbox(name): """Return an mbox iterator given a file/directory/folder name.""" if name == "-": return [get_message(sys.stdin)] if name.startswith("+"): # MH folder name: +folder, +f1,f2,f2, or +ALL name = name[1:] import mhlib mh = mhlib.MH() if name == "ALL": names = mh.listfolders() elif ',' in name: names = name.split(',') else: names = [name] mboxes = [] mhpath = mh.getpath() for name in names: filename = os.path.join(mhpath, name) mbox = mailbox.MHMailbox(filename, get_message) mboxes.append(mbox) if len(mboxes) == 1: return iter(mboxes[0]) else: return _cat(mboxes) elif name.startswith(":"): # IMAP mailbox name: # :username:password@server:folder1,...folderN # :username:password@server:port:folder1,...folderN # :username:password@server:ALL # :username:password@server:port:ALL parts = re.compile( ':(?P[^@:]+):(?P[^@]+)@(?P[^:]+(:[0-9]+)?):(?P[^:]+)' ).match(name).groupdict() from scripts.sb_imapfilter import IMAPSession, IMAPFolder from spambayes import Stats, message from spambayes.Options import options session = IMAPSession(parts['server']) session.login(parts['user'], parts['pwd']) folder_list = session.folder_list() if name == "ALL": names = folder_list else: names = parts['name'].split(',') message_db = message.Message().message_info_db stats = Stats.Stats(options, message_db) mboxes = [IMAPFolder(n, session, stats) for n in names] if len(mboxes) == 1: return full_messages(mboxes[0]) else: return _cat([full_messages(x) for x in mboxes]) if os.path.isdir(name): # XXX Bogus: use a Maildir if /cur is a subdirectory, else a MHMailbox # if the pathname contains /Mail/, else a DirOfTxtFileMailbox. if os.path.exists(os.path.join(name, 'cur')): mbox = mailbox.Maildir(name, get_message) elif name.find("/Mail/") >= 0: mbox = mailbox.MHMailbox(name, get_message) else: mbox = DirOfTxtFileMailbox(name, get_message) else: fp = open(name, "rb") mbox = mailbox.PortableUnixMailbox(fp, get_message) return iter(mbox) def get_message(obj): """Return an email Message object. The argument may be a Message object already, in which case it's returned as-is. If the argument is a string or file-like object (supports read()), the email package is used to create a Message object from it. This can fail if the message is malformed. In that case, the headers (everything through the first blank line) are thrown out, and the rest of the text is wrapped in a bare email.Message.Message. Note that we can't use our own message class here, because this function is imported by tokenizer, and our message class imports tokenizer, so we get a circular import problem. In any case, this function does not need anything that our message class offers, so that shouldn't matter. """ if isinstance(obj, email.Message.Message): return obj # Create an email Message object. if hasattr(obj, "read"): obj = obj.read() try: msg = email.message_from_string(obj) except email.Errors.MessageParseError: # Wrap the raw text in a bare Message object. Since the # headers are most likely damaged, we can't use the email # package to parse them, so just get rid of them first. headers = extract_headers(obj) obj = obj[len(headers):] msg = email.Message.Message() msg.set_payload(obj) return msg def as_string(msg, unixfrom=False): """Convert a Message object to a string in a safe-ish way. In email pkg version 2.5.4 and earlier, msg.as_string() can raise TypeError for some malformed messages. This catches that and attempts to return something approximating the original message. To Do: This really should be done by subclassing email.Message.Message and making this function the as_string() method. After 1.0. [Tony] Better: sb_filter & sb_mboxtrain should stop using this and start using the spambayes.Message classes. They might need a little bit of rearranging, but that should work nicely, and mean that all this code is together in one place. """ if isinstance(msg, str): return msg try: return msg.as_string(unixfrom) except TypeError: ty, val, tb = sys.exc_info() exclines = traceback.format_exception(ty, val, tb)[1:] excstr = " ".join(exclines).strip() headers = [] if unixfrom: headers.append(msg.get_unixfrom()) for (hdr, val) in msg.items(): headers.append("%s: %s" % (hdr, val)) headers.append("X-Spambayes-Exception: %s" % excstr) parts = ["%s\n" % "\n".join(headers)] boundary = msg.get_boundary() for part in msg.get_payload(): if boundary: parts.append(boundary) try: parts.append(part.as_string()) except AttributeError: parts.append(str(part)) if boundary: parts.append("--%s--" % boundary) # make sure it ends with a newline: return "\n".join(parts)+"\n" header_break_re = re.compile(r"\r?\n(\r?\n)") def extract_headers(text): """Very simple-minded header extraction: prefix of text up to blank line. A blank line is recognized via two adjacent line-ending sequences, where a line-ending sequence is a newline optionally preceded by a carriage return. If no blank line is found, all of text is considered to be a potential header section. If a blank line is found, the text up to (but not including) the blank line is considered to be a potential header section. The potential header section is returned, unless it doesn't contain a colon, in which case an empty string is returned. >>> extract_headers("abc") '' >>> extract_headers("abc\\n\\n\\n") # no colon '' >>> extract_headers("abc: xyz\\n\\n\\n") 'abc: xyz\\n' >>> extract_headers("abc: xyz\\r\\n\\r\\n\\r\\n") 'abc: xyz\\r\\n' >>> extract_headers("a: b\\ngibberish\\n\\nmore gibberish") 'a: b\\ngibberish\\n' """ m = header_break_re.search(text) if m: eol = m.start(1) text = text[:eol] if ':' not in text: text = "" return text if __name__ == "__main__": import doctest doctest.testmod() spambayes-1.1a6/spambayes/message.py0000664000076500000240000006643411355063622017663 0ustar skipstaff00000000000000#! /usr/bin/env python """message.py - Core Spambayes classes. Classes: Message - an email.Message.Message, extended with spambayes methods SBHeaderMessage - A Message with spambayes header manipulations MessageInfoDB - persistent state storage for Message, using dbm MessageInfoZODB - persistent state storage for Message, using ZODB MessageInfoPickle - persistent state storage for Message, using pickle Abstract: MessageInfoDB is a simple shelve persistency class for the persistent state of a Message obect. The MessageInfoDB currently does not provide iterators, but should at some point. This would allow us to, for example, see how many messages have been trained differently than their classification, for fp/fn assessment purposes. Message is an extension of the email package Message class, to include persistent message information. The persistent state currently consists of the message id, its current classification, and its current training. The payload is not persisted. SBHeaderMessage extends Message to include spambayes header specific manipulations. Usage: A typical classification usage pattern would be something like: >>> import email >>> # substance comes from somewhere else >>> msg = email.message_from_string(substance, _class=SBHeaderMessage) >>> id = msg.setIdFromPayload() >>> if id is None: >>> msg.setId(time()) # or some unique identifier >>> msg.delSBHeaders() # never include sb headers in a classification >>> # bayes object is your responsibility >>> (prob, clues) = bayes.spamprob(msg.asTokens(), evidence=True) >>> msg.addSBHeaders(prob, clues) A typical usage pattern to train as spam would be something like: >>> import email >>> # substance comes from somewhere else >>> msg = email.message_from_string(substance, _class=SBHeaderMessage) >>> id = msg.setId(msgid) # id is a fname, outlook msg id, something... >>> msg.delSBHeaders() # never include sb headers in a train >>> if msg.getTraining() == False: # could be None, can't do boolean test >>> bayes.unlearn(msg.asTokens(), False) # untrain the ham >>> bayes.learn(msg.asTokens(), True) # train as spam >>> msg.rememberTraining(True) To Do: o Suggestions? """ # This module is part of the spambayes project, which is Copyright 2002-2007 # The Python Software Foundation and is covered by the Python Software # Foundation license. from __future__ import generators __author__ = "Tim Stone " __credits__ = "Mark Hammond, Tony Meyer, all the spambayes contributors." import sys import types import time import math import re import errno import shelve import warnings import cPickle as pickle import traceback import email.Message import email.Parser import email.Header import email.Generator from spambayes import storage from spambayes import dbmstorage from spambayes.tokenizer import tokenize from spambayes.Options import options from spambayes.safepickle import pickle_read, pickle_write try: import cStringIO as StringIO except ImportError: import StringIO CRLF_RE = re.compile(r'\r\n|\r|\n') STATS_START_KEY = "Statistics start date" STATS_STORAGE_KEY = "Persistent statistics" PERSISTENT_HAM_STRING = 'h' PERSISTENT_SPAM_STRING = 's' PERSISTENT_UNSURE_STRING = 'u' class MessageInfoBase(object): def __init__(self, db_name=None): self.db_name = db_name def __len__(self): return len(self.keys()) def get_statistics_start_date(self): if self.db.has_key(STATS_START_KEY): return self.db[STATS_START_KEY] else: return None def set_statistics_start_date(self, date): self.db[STATS_START_KEY] = date self.store() def get_persistent_statistics(self): if self.db.has_key(STATS_STORAGE_KEY): return self.db[STATS_STORAGE_KEY] else: return None def set_persistent_statistics(self, stats): self.db[STATS_STORAGE_KEY] = stats self.store() def __getstate__(self): return self.db def __setstate__(self, state): self.db = state def load_msg(self, msg): if self.db is not None: key = msg.getDBKey() assert key is not None, "None is not a valid key." try: try: attributes = self.db[key] except pickle.UnpicklingError: # The old-style Outlook message info db didn't use # shelve, so get it straight from the dbm. if hasattr(self, "dbm"): attributes = self.dbm[key] else: raise except KeyError: # Set to None, as it's not there. for att in msg.stored_attributes: # Don't overwrite. if not hasattr(msg, att): setattr(msg, att, None) else: if not isinstance(attributes, types.ListType): # Old-style message info db if isinstance(attributes, types.TupleType): # sb_server/sb_imapfilter, which only handled # storing 'c' and 't'. (msg.c, msg.t) = attributes return elif isinstance(attributes, types.StringTypes): # Outlook plug-in, which only handled storing 't', # and did it as a string. msg.t = {"0" : False, "1" : True}[attributes] return else: print >> sys.stderr, "Unknown message info type", \ attributes sys.exit(1) for att, val in attributes: setattr(msg, att, val) def store_msg(self, msg): if self.db is not None: msg.date_modified = time.time() attributes = [] for att in msg.stored_attributes: attributes.append((att, getattr(msg, att))) key = msg.getDBKey() assert key is not None, "None is not a valid key." self.db[key] = attributes self.store() def remove_msg(self, msg): if self.db is not None: del self.db[msg.getDBKey()] self.store() def keys(self): return self.db.keys() class MessageInfoPickle(MessageInfoBase): def __init__(self, db_name, pickle_type=1): MessageInfoBase.__init__(self, db_name) self.mode = pickle_type self.load() def load(self): try: self.db = pickle_read(self.db_name) except IOError, e: if e.errno == errno.ENOENT: # New pickle self.db = {} else: raise def close(self): # we keep no resources open - nothing to do pass def store(self): pickle_write(self.db_name, self.db, self.mode) class MessageInfoDB(MessageInfoBase): def __init__(self, db_name, mode='c'): MessageInfoBase.__init__(self, db_name) self.mode = mode self.load() def load(self): try: self.dbm = dbmstorage.open(self.db_name, self.mode) self.db = shelve.Shelf(self.dbm) except dbmstorage.error: # This probably means that we don't have a dbm module # available. Print out a warning, and continue on # (not persisting any of this data). if options["globals", "verbose"]: print "Warning: no dbm modules available for MessageInfoDB" self.dbm = self.db = None def __del__(self): self.close() def close(self): # Close our underlying database. Better not assume all databases # have close functions! def noop(): pass getattr(self.db, "close", noop)() getattr(self.dbm, "close", noop)() def store(self): if self.db is not None: self.db.sync() # If ZODB isn't available, then this class won't be useable, but we # still need to be able to import this module. So we pretend that all # is ok. try: from persistent import Persistent except ImportError: Persistent = object class _PersistentMessageInfo(MessageInfoBase, Persistent): def __init__(self): import ZODB from BTrees.OOBTree import OOBTree MessageInfoBase.__init__(self) self.db = OOBTree() class MessageInfoZODB(storage.ZODBClassifier): ClassifierClass = _PersistentMessageInfo def __init__(self, db_name, mode='c'): self.nham = self.nspam = 0 # Only used for debugging prints storage.ZODBClassifier.__init__(self, db_name, mode) self.classifier.store = self.store self.db = self.classifier def __setattr__(self, att, value): # Override ZODBClassifier.__setattr__ object.__setattr__(self, att, value) # values are classifier class, True if it accepts a mode # arg, and True if the argument is a pathname _storage_types = {"dbm" : (MessageInfoDB, True, True), "pickle" : (MessageInfoPickle, False, True), ## "pgsql" : (MessageInfoPG, False, False), ## "mysql" : (MessageInfoMySQL, False, False), ## "cdb" : (MessageInfoCDB, False, True), "zodb" : (MessageInfoZODB, True, True), ## "zeo" : (MessageInfoZEO, False, False), } def open_storage(data_source_name, db_type="dbm", mode=None): """Return a storage object appropriate to the given parameters.""" try: klass, supports_mode, unused = _storage_types[db_type] except KeyError: raise storage.NoSuchClassifierError(db_type) if supports_mode and mode is not None: return klass(data_source_name, mode) else: return klass(data_source_name) def database_type(): dn = ("Storage", "messageinfo_storage_file") # The storage options here may lag behind those in storage.py, # so we try and be more robust. If we can't use the same storage # method, then we fall back to pickle. nm, typ = storage.database_type((), default_name=dn) if typ not in _storage_types.keys(): typ = "pickle" return nm, typ class Message(object, email.Message.Message): '''An email.Message.Message extended for SpamBayes''' def __init__(self, id=None): email.Message.Message.__init__(self) # persistent state # (non-persistent state includes all of email.Message.Message state) self.stored_attributes = ['c', 't', 'date_modified', ] self.getDBKey = self.getId self.id = None self.c = None self.t = None self.date_modified = None if id is not None: self.setId(id) # This whole message info database thing is a real mess. It really # ought to be a property of the Message class, not each instance. # So we want to access it via classmethods. However, we have treated # it as a regular attribute, so need to make it a property. To make # a classmethod property, we have to jump through some hoops, which we # deserve for not doing it right in the first place. _message_info_db = None def _get_class_message_info_db(klass): # If, the first time we access the attribute, it hasn't been # set, then we load up the default one. if klass._message_info_db is None: nm, typ = database_type() klass._message_info_db = open_storage(nm, typ) return klass._message_info_db _get_class_message_info_db = classmethod(_get_class_message_info_db) def _set_class_message_info_db(klass, value): klass._message_info_db = value _set_class_message_info_db = classmethod(_set_class_message_info_db) def _get_message_info_db(self): return self._get_class_message_info_db() def _set_message_info_db(self, value): self._set_class_message_info_db(value) message_info_db = property(_get_message_info_db, _set_message_info_db) # This function (and it's hackishness) can be avoided by using # email.message_from_string(text, _class=SBHeaderMessage) # i.e. instead of doing this: # >>> msg = spambayes.message.SBHeaderMessage() # >>> msg.setPayload(substance) # you do this: # >>> msg = email.message_from_string(substance, _class=SBHeaderMessage) # imapfilter has an example of this in action def setPayload(self, payload): """DEPRECATED. This function does not work (as a result of using private methods in a hackish way) in Python 2.4, so is now deprecated. Use *_from_string as described above. More: Python 2.4 has a new email package, and the private functions are gone. So this won't even work. We have to do something to get this to work, for the 1.0.x branch, so use a different ugly hack. """ warnings.warn("setPayload is deprecated. Use " \ "email.message_from_string(payload, _class=" \ "Message) instead.", DeprecationWarning, 2) new_me = email.message_from_string(payload, _class=Message) self.__dict__.update(new_me.__dict__) def setId(self, id): if self.id and self.id != id: raise ValueError, ("MsgId has already been set," " cannot be changed %r %r") % (self.id, id) if id is None: raise ValueError, "MsgId must not be None" if not type(id) in types.StringTypes: raise TypeError, "Id must be a string" if id == STATS_START_KEY: raise ValueError, "MsgId must not be " + STATS_START_KEY if id == STATS_STORAGE_KEY: raise ValueError, "MsgId must not be " + STATS_STORAGE_KEY self.id = id self.message_info_db.load_msg(self) def getId(self): return self.id def tokenize(self): return tokenize(self) def _force_CRLF(self, data): """Make sure data uses CRLF for line termination.""" return CRLF_RE.sub('\r\n', data) def as_string(self, unixfrom=False, mangle_from_=True): # The email package stores line endings in the "internal" Python # format ('\n'). It is up to whoever transmits that information to # convert to appropriate line endings (according to RFC822, that is # \r\n *only*). imaplib *should* take care of this for us (in the # append function), but does not, so we do it here try: fp = StringIO.StringIO() g = email.Generator.Generator(fp, mangle_from_=mangle_from_) g.flatten(self, unixfrom) return self._force_CRLF(fp.getvalue()) except TypeError: parts = [] for part in self.get_payload(): parts.append(email.Message.Message.as_string(part, unixfrom)) return self._force_CRLF("\n".join(parts)) def modified(self): if self.id: # only persist if key is present self.message_info_db.store_msg(self) def GetClassification(self): if self.c == PERSISTENT_SPAM_STRING: return options['Headers', 'header_spam_string'] elif self.c == PERSISTENT_HAM_STRING: return options['Headers', 'header_ham_string'] elif self.c == PERSISTENT_UNSURE_STRING: return options['Headers', 'header_unsure_string'] return None def RememberClassification(self, cls): # this must store state independent of options settings, as they # may change, which would really screw this database up if cls == options['Headers', 'header_spam_string']: self.c = PERSISTENT_SPAM_STRING elif cls == options['Headers', 'header_ham_string']: self.c = PERSISTENT_HAM_STRING elif cls == options['Headers', 'header_unsure_string']: self.c = PERSISTENT_UNSURE_STRING else: raise ValueError, \ "Classification must match header strings in options" self.modified() def GetTrained(self): return self.t def RememberTrained(self, isSpam): # isSpam == None means no training has been done self.t = isSpam self.modified() def __repr__(self): return "spambayes.message.Message%r" % repr(self.__getstate__()) def __getstate__(self): return (self.id, self.c, self.t) def __setstate__(self, t): (self.id, self.c, self.t) = t class SBHeaderMessage(Message): '''Message class that is cognizant of SpamBayes headers. Adds routines to add/remove headers for SpamBayes''' def setPayload(self, payload): """DEPRECATED. """ warnings.warn("setPayload is deprecated. Use " \ "email.message_from_string(payload, _class=" \ "SBHeaderMessage) instead.", DeprecationWarning, 2) new_me = email.message_from_string(payload, _class=SBHeaderMessage) self.__dict__.update(new_me.__dict__) def setIdFromPayload(self): try: self.setId(self[options['Headers', 'mailid_header_name']]) except ValueError: return None return self.id def setDisposition(self, prob): if prob < options['Categorization', 'ham_cutoff']: disposition = options['Headers', 'header_ham_string'] elif prob > options['Categorization', 'spam_cutoff']: disposition = options['Headers', 'header_spam_string'] else: disposition = options['Headers', 'header_unsure_string'] self.RememberClassification(disposition) def addSBHeaders(self, prob, clues): """Add hammie header, and remember message's classification. Also, add optional headers if needed.""" self.setDisposition(prob) disposition = self.GetClassification() self[options['Headers', 'classification_header_name']] = disposition if options['Headers', 'include_score']: disp = "%.*f" % (options["Headers", "header_score_digits"], prob) if options["Headers", "header_score_logarithm"]: if prob <= 0.005 and prob > 0.0: x = -math.log10(prob) disp += " (%d)" % x if prob >= 0.995 and prob < 1.0: x = -math.log10(1.0-prob) disp += " (%d)" % x self[options['Headers', 'score_header_name']] = disp if options['Headers', 'include_thermostat']: thermostat = '**********' self[options['Headers', 'thermostat_header_name']] = \ thermostat[:int(prob*10)] if options['Headers', 'include_evidence']: hco = options['Headers', 'clue_mailheader_cutoff'] sco = 1 - hco evd = [] for word, score in clues: if (word == '*H*' or word == '*S*' \ or score <= hco or score >= sco): if isinstance(word, types.UnicodeType): word = email.Header.Header(word, charset='utf-8').encode() try: evd.append("%r: %.2f" % (word, score)) except TypeError: evd.append("%r: %s" % (word, score)) # Line-wrap this header, because it can get very long. We don't # use email.Header.Header because that can explode with unencoded # non-ASCII characters. We can't use textwrap because that's 2.3. wrappedEvd = [] headerName = options['Headers', 'evidence_header_name'] lineLength = len(headerName) + len(': ') for component, index in zip(evd, range(len(evd))): wrappedEvd.append(component) lineLength += len(component) if index < len(evd)-1: if lineLength + len('; ') + len(evd[index+1]) < 78: wrappedEvd.append('; ') else: wrappedEvd.append(';\n\t') lineLength = 8 self[headerName] = "".join(wrappedEvd) if options['Headers', 'add_unique_id']: self[options['Headers', 'mailid_header_name']] = self.id self.addNotations() def addNotations(self): """Add the appropriate string to the subject: and/or to: header. This is a reasonably ugly method of including the classification, but no-one has a better idea about how to allow filtering in 'stripped down' mailers (i.e. Outlook Express), so, for the moment, this is it. """ disposition = self.GetClassification() # options["Headers", "notate_to"] (and notate_subject) can be # either a single string (like "spam") or a tuple (like # ("unsure", "spam")). In Python 2.3 checking for a string in # something that could be a string or a tuple works fine, but # it dies in Python 2.2, because you can't do 'string in string', # only 'character in string', so we allow for that. if isinstance(options["Headers", "notate_to"], types.StringTypes): notate_to = (options["Headers", "notate_to"],) else: notate_to = options["Headers", "notate_to"] if disposition in notate_to: # Once, we treated the To: header just like the Subject: one, # but that doesn't really make sense - and OE stripped the # comma that we added, treating it as a separator, so it # wasn't much use anyway. So we now convert the classification # to an invalid address, and add that. address = "%s@spambayes.invalid" % (disposition, ) try: self.replace_header("To", "%s,%s" % (address, self["To"])) except KeyError: self["To"] = address if isinstance(options["Headers", "notate_subject"], types.StringTypes): notate_subject = (options["Headers", "notate_subject"],) else: notate_subject = options["Headers", "notate_subject"] if disposition in notate_subject: try: self.replace_header("Subject", "%s,%s" % (disposition, self["Subject"])) except KeyError: self["Subject"] = disposition def delNotations(self): """If present, remove our notation from the subject: and/or to: header of the message. This is somewhat problematic, as we cannot be 100% positive that we added the notation. It's almost certain to be us with the to: header, but someone else might have played with the subject: header. However, as long as the user doesn't turn this option on and off, this will all work nicely. See also [ 848365 ] Remove subject annotations from message review page """ subject = self["Subject"] if subject: ham = options["Headers", "header_ham_string"] + ',' spam = options["Headers", "header_spam_string"] + ',' unsure = options["Headers", "header_unsure_string"] + ',' if options["Headers", "notate_subject"]: for disp in (ham, spam, unsure): if subject.startswith(disp): self.replace_header("Subject", subject[len(disp):]) # Only remove one, maximum. break to = self["To"] if to: ham = "%s@spambayes.invalid," % \ (options["Headers", "header_ham_string"],) spam = "%s@spambayes.invalid," % \ (options["Headers", "header_spam_string"],) unsure = "%s@spambayes.invalid," % \ (options["Headers", "header_unsure_string"],) if options["Headers", "notate_to"]: for disp in (ham, spam, unsure): if to.startswith(disp): self.replace_header("To", to[len(disp):]) # Only remove one, maximum. break def currentSBHeaders(self): """Return a dictionary containing the current values of the SpamBayes headers. This can be used to restore the values after using the delSBHeaders() function.""" headers = {} for header_name in [options['Headers', 'classification_header_name'], options['Headers', 'mailid_header_name'], (options['Headers', 'classification_header_name'] + "-ID"), options['Headers', 'thermostat_header_name'], options['Headers', 'evidence_header_name'], options['Headers', 'score_header_name'], options['Headers', 'trained_header_name'], ]: value = self[header_name] if value is not None: headers[header_name] = value return headers def delSBHeaders(self): del self[options['Headers', 'classification_header_name']] del self[options['Headers', 'mailid_header_name']] del self[options['Headers', 'classification_header_name'] + "-ID"] # test mode header del self[options['Headers', 'thermostat_header_name']] del self[options['Headers', 'evidence_header_name']] del self[options['Headers', 'score_header_name']] del self[options['Headers', 'trained_header_name']] # Also delete notations - typically this is called just before # training, and we don't want them there for that. self.delNotations() # Utility function to insert an exception header into the given RFC822 text. # This is used by both sb_server and sb_imapfilter, so it's handy to have # it available separately. def insert_exception_header(string_msg, msg_id=None): """Insert an exception header into the given RFC822 message (as text). Returns a tuple of the new message text and the exception details.""" stream = StringIO.StringIO() traceback.print_exc(None, stream) details = stream.getvalue() # Build the header. This will strip leading whitespace from # the lines, so we add a leading dot to maintain indentation. detailLines = details.strip().split('\n') dottedDetails = '\n.'.join(detailLines) headerName = 'X-Spambayes-Exception' header = email.Header.Header(dottedDetails, header_name=headerName) # Insert the exception header, and optionally also insert the id header, # otherwise we might keep doing this message over and over again. # We also ensure that the line endings are /r/n as RFC822 requires. try: headers, body = re.split(r'\n\r?\n', string_msg, 1) except ValueError: # No body - this is a bad message! headers = string_msg body = "" header = re.sub(r'\r?\n', '\r\n', str(header)) headers += "\n%s: %s\r\n" % (headerName, header) if msg_id: headers += "%s: %s\r\n" % \ (options["Headers", "mailid_header_name"], msg_id) return (headers + '\r\n' + body, details) spambayes-1.1a6/spambayes/MoinSecurityPolicy.py0000664000076500000240000002200111112142467022023 0ustar skipstaff00000000000000#!/usr/bin/env python # -*- coding: iso-8859-1 -*- """ This module implements a security policy for MoinMoin based on the SpamBayes classifier. To use it, import it like so in your wikiconfig.py file: from spambayes.MoinSecurityPolicy import SecurityPolicy Two pages are special, HamPages and SpamPages. Each refers to a specific revision of the raw version of particulars wiki pages. When either of these pages is updated the SpamBayes database is rebuilt based on the pages they reference. When any other page is updated it is scored against the current database. If its score is <= SecurityPolicy.ham_cutoff the edit is accepted. If not, the page edit is accepted but reverted and a reference to the reverted page revision is mailed to the members of the AdminGroup for review. If it is only possibly spam (score between SecurityPolicy.ham_cutoff and SecurityPolicy.spam_cutoff) the recipients are instructed to add it to either HamPages or SpamPages as appropriate. If it is truly spam (score >= SecurityPolicy.spam_cutoff), the recipients are instructed to add it to HamPages if it is actually okay, but to simply discard it otherwise. The HamPages and SpamPages pages are formatted as any other *Group page, a top-level list forms a group while everything else is ignored. The ham_cutoff, spam_cutoff and spam_db attributes are defined at the class level to make it easy for the user to change their values. The defaults are: ham_cutoff 0.15 spam_cutoff 0.60 spam_db 'spam.db' The spam_db attribute should always be a relative path (should not start with '/'). When relative it will be taken relative to the directory containing the event-log file. """ import os import atexit import urlparse from MoinMoin.security import Permissions from MoinMoin.wikidicts import Group from MoinMoin.user import User, getUserId from MoinMoin.util.mail import sendmail from MoinMoin.Page import Page from MoinMoin.PageEditor import PageEditor from spambayes import hammie, storage from spambayes.tokenizer import Tokenizer as _Tokenizer, numeric_entity_re, \ numeric_entity_replacer, crack_urls, breaking_entity_re, html_re, \ tokenize_word class SecurityPolicy(Permissions): ham_cutoff = 0.15 spam_cutoff = 0.60 spam_db = "spam.db" def __init__(self, user): Permissions.__init__(self, user) self.sbayes = None def open_spamdb(self, request): if self.sbayes is None: event_log = request.rootpage.getPagePath('event-log', isfile=1) spam_db = os.path.join(os.path.dirname(event_log), self.spam_db) self.sbayes = Hammie(storage.open_storage(spam_db, "pickle", 'c')) atexit.register(self.close_spamdb) def close_spamdb(self): if self.sbayes is not None: self.sbayes.store() self.sbayes = None def retrain(self, request): self.close_spamdb() if os.path.exists(self.spam_db): os.unlink(self.spam_db) self.open_spamdb(request) nham = nspam = 0 for url in Group(request, "HamPages").members(): scheme, netloc, path, params, query, frag = urlparse.urlparse(url) rev = 0 for pair in query.split("&"): key, val = pair.split("=") if key == "rev": raw = int(val) break pg = Page(request, path[1:], rev=rev) self.sbayes.train_ham(pg.get_raw_body()) nham += 1 for url in Group(request, "SpamPages").members(): scheme, netloc, path, params, query, frag = urlparse.urlparse(url) rev = 0 for pair in query.split("&"): key, val = pair.split("=") if key == "rev": raw = int(val) break pg = Page(request, path[1:], rev=rev) self.sbayes.train_spam(pg.get_raw_body()) nspam += 1 self.close_spamdb() return (nham, nspam) def save(self, editor, newtext, rev, **kw): self.open_spamdb(editor.request) score = self.sbayes.score(newtext) save_result = Permissions.save(self, editor, newtext, rev, **kw) if save_result and editor.page_name in ("HamPages", "SpamPages"): self.retrain(editor.request) return save_result if score < self.ham_cutoff: # File checks out spamwise. Return the default save result. return save_result if not save_result: return save_result # Now the fun begins. We scored the page and found that it is # either possible or probable spam. However, we saved it. (We # wanted to do that so we would have a copy to score later.) We # need to revert the save and send the URL of the suspect page # to the users in AdminGroup. To make matters worse, the user # may have write permission but not revert permission. So we # have to force the reversion. That requires a bit of # cut-n-paste from wikiaction.do_revert. self.force_revert(editor.page_name, editor.request) ## self.mail_admins_about(editor.request, editor.page_name, score) def force_revert(self, pagename, request): rev = int(request.form['rev'][0]) revstr = '%08d' % rev oldpg = Page(request, pagename, rev=rev) pg = PageEditor(request, pagename) _ = request.getText msg = _("Thank you for your changes. Your attention to detail is appreciated.") try: pg._write_file(oldpg.get_raw_body(), action="SAVE/REVERT", extra=revstr) pg.clean_acl_cache() except pg.SaveError, msg: pass # msg contain a unicode string savemsg = unicode(msg) request.reset() pg.send_page(request, msg=savemsg) return None def mail_admins_about(self, request, page_name, score): """Send email to the AdminGroup about a suspect page.""" # This does not yet work. I've yet to figure out how to extract the # email addresses of the members of the AdminGroup. return admin_text = Page(request, "AdminGroup").get_raw_body() group = Group(request, admin_text) emails = [] for name in group.members(): uid = getUserId(request, name) if uid is None: continue u = User(request, uid) emails.append(u.email) if score < self.spam_cutoff: subject = "Possible wiki spam" text = """\ This page as submitted to the wiki might be spam: %(page_name)s If that is not the case, add the page's URL (including action=raw and the revision number) to HamPages then revert the page to that revision. If it is spam, add it instead to SpamPages. """ % locals() else: subject = "Probable wiki spam" text = """\ This page as submitted to the wiki is likely to be spam: %(page_name)s If that is not the case, add the page's URL (including action=raw and the revision number) to HamPages then revert the page to that revision. If it is spam, do nothing. """ % locals() sendmail(request, emails, subject, text) class Tokenizer(_Tokenizer): def tokenize(self, text): """Tokenize a chunk of text. Pulled mostly verbatim from the SpamBayes code. """ maxword = 20 # Replace numeric character entities (like a for the letter # 'a'). text = numeric_entity_re.sub(numeric_entity_replacer, text) # Crack open URLs and extract useful bits of marrow... for cracker in (crack_urls,): text, tokens = cracker(text) for t in tokens: yield t # Remove HTML/XML tags. Also  .
    and

    tags should # create a space too. text = breaking_entity_re.sub(' ', text) # It's important to eliminate HTML tags rather than, e.g., # replace them with a blank (as this code used to do), else # simple tricks like # Wrinkle Reduction # can be used to disguise words.
    and

    were special- # cased just above (because browsers break text on those, # they can't be used to hide words effectively). text = html_re.sub('', text) # Tokenize everything in the body. for w in text.split(): n = len(w) # Make sure this range matches in tokenize_word(). if 3 <= n <= maxword: yield w elif n >= 3: for t in tokenize_word(w): yield t class Hammie(hammie.Hammie): def __init__(self, bayes): hammie.Hammie.__init__(self, bayes) self.tokenizer = Tokenizer() def _scoremsg(self, msg, evidence=False): return self.bayes.spamprob(self.tokenizer.tokenize(msg), evidence) def train(self, msg, is_spam, add_header=False): self.bayes.learn(self.tokenizer.tokenize(msg), is_spam) spambayes-1.1a6/spambayes/msgs.py0000664000076500000240000000617210646440130017174 0ustar skipstaff00000000000000from __future__ import generators import os import random from spambayes.tokenizer import tokenize HAMTEST = None SPAMTEST = None HAMTRAIN = None SPAMTRAIN = None SEED = random.randrange(2000000000) class Msg(object): __slots__ = 'tag', 'guts' def __init__(self, dir, name): path = dir + "/" + name self.tag = path f = open(path, 'rb') self.guts = f.read() f.close() def __iter__(self): return tokenize(self.guts) # Compare msgs by their paths; this is appropriate for sets of msgs. def __hash__(self): return hash(self.tag) def __eq__(self, other): return self.tag == other.tag def __str__(self): return self.guts # We have defined __slots__, so need these to be able to be pickled. def __getstate__(self): return self.tag, self.guts def __setstate__(self, s): self.tag, self.guts = s # The iterator yields a stream of Msg objects, taken from a list of # directories. class MsgStream(object): __slots__ = 'tag', 'directories', 'keep' def __init__(self, tag, directories, keep=None): self.tag = tag self.directories = directories self.keep = keep def __str__(self): return self.tag def produce(self): if self.keep is None: for directory in self.directories: for fname in os.listdir(directory): yield Msg(directory, fname) return # We only want part of the msgs. Shuffle each directory list, but # in such a way that we'll get the same result each time this is # called on the same directory list. for directory in self.directories: all = os.listdir(directory) random.seed(hash(max(all)) ^ SEED) # reproducible across calls random.shuffle(all) del all[self.keep:] all.sort() # seems to speed access on Win98! for fname in all: yield Msg(directory, fname) def __iter__(self): return self.produce() class HamStream(MsgStream): def __init__(self, tag, directories, train=0): if train: MsgStream.__init__(self, tag, directories, HAMTRAIN) else: MsgStream.__init__(self, tag, directories, HAMTEST) class SpamStream(MsgStream): def __init__(self, tag, directories, train=0): if train: MsgStream.__init__(self, tag, directories, SPAMTRAIN) else: MsgStream.__init__(self, tag, directories, SPAMTEST) def setparms(hamtrain, spamtrain, hamtest=None, spamtest=None, seed=None): """Set HAMTEST/TRAIN and SPAMTEST/TRAIN. If seed is not None, also set SEED. If (ham|spam)test are not set, set to the same as the (ham|spam)train numbers (backwards compat option). """ global HAMTEST, SPAMTEST, HAMTRAIN, SPAMTRAIN, SEED HAMTRAIN, SPAMTRAIN = hamtrain, spamtrain if hamtest is None: HAMTEST = HAMTRAIN else: HAMTEST = hamtest if spamtest is None: SPAMTEST = SPAMTRAIN else: SPAMTEST = spamtest if seed is not None: SEED = seed spambayes-1.1a6/spambayes/oe_mailbox.py0000664000076500000240000007144211137470237020352 0ustar skipstaff00000000000000""" Simple Python library for Outlook Express mailbox handling, and some other Outlook Express utility functions. Functions: getDBXFilesList() Returns a list containing the DBX file names for current user getMbox(dbxPath) Returns an mbox converted from a DBX file getRegistryKey() Returns the root key for current user's Outlook Express settings getStorePath() Returns the path where DBX files are stored for current user train(dbxPath, isSpam) Trains a DBX file as spam or ham through Hammie """ from __future__ import generators # This module is part of the spambayes project, which is Copyright 2002-2007 # The Python Software Foundation and is covered by the Python Software # Foundation license. __author__ = "Romain Guy " __credits__ = "All the SpamBayes folk" # Based on C++ work by Arne Schloh import sys import binascii import os import re import struct import random from time import * try: import cStringIO as StringIO except ImportError: import StringIO from spambayes import msgs try: import win32api import win32con import win32gui from win32com.shell import shell, shellcon except ImportError: # Not win32, or pywin32 not installed. # Some functions will not work, but some will. win32api = win32con = win32gui = shell = shellcon = None ########################################################################### ## DBX FILE HEADER ########################################################################### class dbxFileHeader: """ Each Outlook Express DBX file has a file header. This header defines many properties, only a few of which interest us. The only properties which are required are defined by indexes. The indexes are static attributes of the class and their names begin with "fh". You can access their values through the method getEntry(). """ HEADER_SIZE = 0x24bc # total header size HEADER_ENTRIES = HEADER_SIZE >> 2 # total of entries in the header MAGIC_NUMBER = 0xfe12adcfL # specific to DBX files OFFLINE = 0x26fe9d30L # specific to offline.dbx FOLDERS = 0x6f74fdc6L # specific to folders.dbx POP3UIDL = 0x6f74fdc7L # specific to pop3uidl.dbx # various entries indexes FH_FILE_INFO_LENGTH = 0x07 # file info length FH_FIRST_FOLDER_LIST_NODE = 0x1b # pointer to the first folder list node FH_LAST_FOLDER_LIST_NODE = 0x1c # pointer to the last folder list node FH_MESSAGE_CONDITIONS_PTR = 0x22 # pointer to the message conditions object FH_FOLDER_CONDITIONS_PTR = 0x23 # pointer to the folder conditions object FH_ENTRIES = 0x31 # entries in tree FH_TREE_ROOT_NODE_PTR = 0x39 # pointer to the root node of a tree FILE_HEADER_ENTRIES = \ [ ( 0x07, "file info length" ), ( 0x09, "pointer to the last variable segment" ), ( 0x0a, "length of a variable segment" ), ( 0x0b, "used space of the last variable segment" ), ( 0x0c, "pointer to the last tree segment" ), ( 0x0d, "length of a tree segment" ), ( 0x0e, "used space of the last tree segment" ), ( 0x0f, "pointer to the last message segment" ), ( 0x10, "length of a message segment" ), ( 0x11, "used space of the last message segment" ), ( 0x12, "root pointer to the deleted message list" ), ( 0x13, "root pointer to the deleted tree list" ), ( 0x15, "used space in the middle sector of the file" ), ( 0x16, "reusable space in the middle sector of the file" ), ( 0x17, "index of the last entry in the tree" ), ( 0x1b, "pointer to the first folder list node" ), ( 0x1c, "pointer to the last folder list node" ), ( 0x1f, "used space of the file" ), ( 0x22, "pointer to the message conditions object" ), ( 0x23, "pointer to the folder conditions object" ), ( 0x31, "entries in the tree" ), ( 0x32, "entries in the 2.nd tree" ), ( 0x33, "entries in the 3.rd tree" ), ( 0x39, "pointer to the root node of the tree" ), ( 0x3a, "pointer to the root node of the 2.nd tree" ), ( 0x3b, "pointer to the root node of the 3.rd tree" ), ( 0x9f, "used space for indexed info objects" ), ( 0xa0, "used space for conditions objects" ), ( 0xa2, "used space for folder list objects" ), ( 0xa3, "used space for tree objects" ), ( 0xa4, "used space for message objects" )] def __init__(self, dbxStream): """Initialize the DBX header by reading it directly from the passed stream.""" dbxStream.seek(0) self.dbxBuffer = dbxStream.read(dbxFileHeader.HEADER_SIZE) def isMessages(self): """Return true iff the DBX is a messages DBX.""" return not (self.isFolders() or self.isPOP3UIDL() or self.isOffline()) def isFolders(self): """Return true if the DBX is the folders DBX.""" return self.getEntry(1) == dbxFileHeader.FOLDERS def isPOP3UIDL(self): """Return true if the DBX is the POP3UIDL DBX.""" return self.getEntry(1) == dbxFileHeader.POP3UIDL def isOffline(self): """Return true if the DBX is the offline DBX.""" return self.getEntry(1) == dbxFileHeader.OFFLINE def isValid(self): """Return true if the DBX is a valid DBX file.""" return self.getEntry(0) == dbxFileHeader.MAGIC_NUMBER def getHeaderBuffer(self): """Return the bytes buffer containing the whole header.""" return self.dbxBuffer def getEntry(self, dbxEntry): """Return the n-th entry as a long integer.""" return struct.unpack("L", self.dbxBuffer[dbxEntry * 4:(dbxEntry * 4) + 4])[0] def getEntryAsHexStr(self, dbxEntry): """Return the n-th entry as an hexadecimal string. (Little endian encoding!)""" return '0x' + \ binascii.hexlify(self.dbxBuffer[dbxEntry * 4:(dbxEntry * 4) + 4]) ########################################################################### ## DBX FILE INFO ########################################################################### class dbxFileInfo: """ Following the DBX header there is DBX info. This part gives the name of the folder described by the current DBX. """ MESSAGE_FILE_INFO = 0x618 def __init__(self, dbxStream, dbxLength): """Reads the DBX info part from a DBX stream.""" dbxStream.seek(dbxFileHeader.HEADER_SIZE) self.dbxLength = dbxLength self.dbxBuffer = dbxStream.read(dbxLength) def isFoldersInfo(self): """Return true if the info belongs to folders.dbx.""" return self.dbxLength != dbxFileInfo.MESSAGE_FILE_INFO def getFolderName(self): """Returns the folder name.""" if not self.isFoldersInfo(): name = [c for c in self.dbxBuffer[0x105:0x210] if ord(c) != 0] return "".join(name) else: return None def getCreationTime(self): """Not implemented yet.""" if self.isFoldersInfo(): return "Not implemented yet" else: return None ########################################################################### ## DBX TREE ########################################################################### class dbxTree: """Stands for the tree which stores the messages in a given folder.""" TREE_NODE_SIZE = 0x27c # size of a tree node def __init__(self, dbxStream, dbxAddress, dbxValues): """Reads the addresses of the stored messages.""" self.dbxValues = [i for i in range(dbxValues)] # XXX : silly fix ! if dbxAddress > 0: self.__readValues(dbxStream, 0, dbxAddress, 0, dbxValues) def __readValues(self, dbxStream, unused, dbxAddress, dbxPosition, unused2): dbxStream.seek(dbxAddress) dbxBuffer = dbxStream.read(dbxTree.TREE_NODE_SIZE) count = 0 entries = ((self.getEntry(dbxBuffer, 4) >> 8) & 0xff) if self.getEntry(dbxBuffer, 2) != 0: self.__readValues(dbxStream, dbxAddress, self.getEntry(dbxBuffer, 2), dbxPosition, self.getEntry(dbxBuffer, 5)) count += self.getEntry(dbxBuffer, 5) for i in range(entries): pos = 6 + i * 3 if self.getEntry(dbxBuffer, pos) != 0: count += 1 value = dbxPosition + count self.dbxValues[value - 1] = self.getEntry(dbxBuffer, pos) if self.getEntry(dbxBuffer, pos + 1) != 0: self.__readValues(dbxStream, dbxAddress, self.getEntry(dbxBuffer, pos + 1), dbxPosition + count, self.getEntry(dbxBuffer, pos + 2)) count += self.getEntry(dbxBuffer, pos + 2) def getEntry(self, dbxBuffer, dbxEntry): """Return the n-th entry as a long integer.""" return struct.unpack("L", dbxBuffer[dbxEntry * 4:(dbxEntry * 4) + 4])[0] def getValue(self, dbxIndex): """Return the address of the n-th message.""" return self.dbxValues[dbxIndex] ########################################################################### ## DBX INDEXED INFO ########################################################################### class dbxIndexedInfo: """ Messages and folders mailboxes contain the "message info" and "folders info" entities. These entities are indexed info sequences. This is their base class. """ MAX_INDEX = 0x20 # max index DT_NONE = 0 # data type none def __init__(self, dbxStream, dbxAddress): """Reads the indexed infos from the passed stream.""" self.dbxBodyLength = 0L self.dbxObjectLength = 0L self.dbxEntries = 0L self.dbxCounter = 0L self.dbxBuffer = [] self.dbxIndexes = 0L self.dbxBegin = [0L for i in range(dbxIndexedInfo.MAX_INDEX)] self.dbxLength = [i for i in self.dbxBegin] self.dbxAddress = dbxAddress self.__readIndexedInfo(dbxStream) def __readIndexedInfo(self, dbxStream): dbxStream.seek(self.dbxAddress) temp = dbxStream.read(12) self.dbxBodyLength = self.__getEntry(temp, 1) self.dbxObjectLength = self.__getEntry(temp, 2) & 0xffff self.dbxEntries = (self.__getEntry(temp, 2) >> 16) & 0xff self.dbxCounter = (self.__getEntry(temp, 1) >> 24) & 0xff self.dbxBuffer = dbxStream.read(self.dbxBodyLength) # bytes array isIndirect = bool(0) # boolean lastIndirect = 0 data = self.dbxEntries << 2 # index within dbxBuffer for i in range(self.dbxEntries): value = self.__getEntry(self.dbxBuffer, i) isDirect = value & 0x80 index = value & 0x7f value >>= 8 if isDirect: self.__setIndex(index, (i << 2) + 1, 3) else: self.__setIndex(index, data + value) if isIndirect: self.__setEnd(lastIndirect, data + value) isIndirect = bool(1) lastIndirect = index self.dbxIndexes |= 1 << index if isIndirect: self.__setEnd(lastIndirect, self.dbxBodyLength) def __setIndex(self, dbxIndex, dbxBegin, dbxLength = 0): if dbxIndex < dbxIndexedInfo.MAX_INDEX: self.dbxBegin[dbxIndex] = dbxBegin self.dbxLength[dbxIndex] = dbxLength def __setEnd(self, dbxIndex, dbxEnd): if dbxIndex < dbxIndexedInfo.MAX_INDEX: self.dbxLength[dbxIndex] = dbxEnd - self.dbxBegin[dbxIndex] def __getEntry(self, dbxBuffer, dbxEntry): return struct.unpack("L", dbxBuffer[dbxEntry * 4:(dbxEntry * 4) + 4])[0] def getIndexText(self, dbxIndex): """Returns the description of the given indexed field.""" return "" def getIndexDataType(self, dbxIndex): """Returns the data type of the given index.""" return self.DT_NONE def getValue(self, dbxIndex): """Returns a tuple : (index in buffer of the info, length of the info).""" return (self.dbxBegin[dbxIndex], self.dbxLength[dbxIndex]) def getValueAsLong(self, dbxIndex): """Returns the indexed info as a long value.""" data, length = self.getValue(dbxIndex) value = 0 if data: value = struct.unpack("L", self.dbxBuffer[data:data + 4])[0] if length < 4: value &= (1 << (length << 3)) - 1 return value def getString(self, dbxIndex): """Returns the indexed info as a string value.""" index = self.dbxBegin[dbxIndex] end = index for c in self.dbxBuffer[index:]: if ord(c) == 0: break end += 1 return self.dbxBuffer[index:end] def getAddress(self): return self.dbxAddress def getBodyLength(self): return self.dbxBodyLength def getEntries(self): return self.dbxEntries def getCounter(self): return self.dbxCounter def getIndexes(self): return self.dbxIndexes def isIndexed(self, dbxIndex): return self.dbxIndexes & (1 << dbxIndex) ########################################################################### ## DBX MESSAGE INFO ########################################################################### class dbxMessageInfo(dbxIndexedInfo): """ The message info structure inherits from the index info one. It just defines extra constants which allow to access pertinent info. """ MI_INDEX = 0x0 # index of the message MI_FLAGS = 0x1 # the message flags MI_MESSAGE_ADDRESS = 0x4 # the address of the message MI_SUBJECT = 0x8 # the subject of the message # label of each indexed info INDEX_LABEL = \ [ "message index" , "flags" , "time message created/send" , "body lines" , "message address" , "original subject" , "time message saved" , "message id" , "subject" , "sender eMail address and name" , "answered to message id" , "server/newsgroup/message number", "server" , "sender name" , "sender eMail address" , "id 0f" , "message priority" , "message text length" , "time message created/received", "receiver name" , "receiver eMail address" , "id 15" , "id 16" , "id 17" , "id 18" , "id 19" , "OE account name" , "OE account registry key" , "message text structure" , "id 1d" , "id 1e" , "id 1f" ] DT_NONE = 0 # index is none DT_INT4 = 1 # index is a long integer (32 bits) DT_STRING = 2 # index is a string DT_DATE_TIME = 3 # index is date/time DT_DATA = 4 # index is data # the data type of each index INDEX_DATA_TYPE = \ [ DT_INT4 , DT_INT4 , DT_DATE_TIME, DT_INT4 , DT_INT4 , DT_STRING, DT_DATE_TIME, DT_STRING, DT_STRING, DT_STRING, DT_STRING , DT_STRING, DT_STRING, DT_STRING, DT_STRING , DT_NONE , DT_INT4 , DT_INT4 , DT_DATE_TIME, DT_STRING, DT_STRING, DT_NONE , DT_INT4 , DT_NONE , DT_INT4 , DT_INT4 , DT_STRING , DT_STRING, DT_DATA , DT_NONE , DT_NONE , DT_NONE ] def getIndexText(self, dbxIndex): return dbxMessageInfo.INDEX_LABEL[dbxIndex] def getIndexDataType(self, dbxIndex): return dbxMessageInfo.INDEX_DATA_TYPE[dbxIndex] ########################################################################### ## DBX MESSAGE ########################################################################### class dbxMessage: def __init__(self, dbxStream, dbxAddress): self.dbxAddress = dbxAddress self.dbxText = "" self.dbxLength = 0L self.__readMessageText(dbxStream) def __getEntry(self, dbxBuffer, dbxEntry): if len(dbxBuffer) < (dbxEntry * 4) + 4: return None return struct.unpack("L", dbxBuffer[dbxEntry * 4:(dbxEntry * 4) + 4])[0] def __readMessageText(self, dbxStream): address = self.dbxAddress header = "" while (address): dbxStream.seek(address) header = dbxStream.read(16) self.dbxLength += self.__getEntry(header, 2) address = self.__getEntry(header, 3) pos = "" address = self.dbxAddress while (address): dbxStream.seek(address) header = dbxStream.read(16) pos += dbxStream.read(self.__getEntry(header, 2)) address = self.__getEntry(header, 3) self.dbxText = pos def getText(self): return self.dbxText # This started its SpamBayes life as a private method of the UserInterface # class, but is really a general purpose (Outlook Express) function. def convertToMbox(content): """Check if the given buffer is in a non-mbox format, and convert it into mbox format if so. If it's already an mbox, return it unchanged. """ dbxStream = StringIO.StringIO(content) header = dbxFileHeader(dbxStream) if header.isValid() and header.isMessages(): file_info_len = dbxFileHeader.FH_FILE_INFO_LENGTH fh_entries = dbxFileHeader.FH_ENTRIES fh_ptr = dbxFileHeader.FH_TREE_ROOT_NODE_PTR info = dbxFileInfo(dbxStream, header.getEntry(file_info_len)) entries = header.getEntry(fh_entries) address = header.getEntry(fh_ptr) if address and entries: tree = dbxTree(dbxStream, address, entries) dbxBuffer = [] for i in range(entries): address = tree.getValue(i) messageInfo = dbxMessageInfo(dbxStream, address) if messageInfo.isIndexed(dbxMessageInfo.MI_MESSAGE_ADDRESS): address = dbxMessageInfo.MI_MESSAGE_ADDRESS messageAddress = messageInfo.getValueAsLong(address) message = dbxMessage(dbxStream, messageAddress) # This fakes up a from header to conform to mbox # standards. It would be better to extract this # data from the message itself, as this will # result in incorrect tokens. dbxBuffer.append("From spambayes@spambayes.org %s\n%s" \ % (strftime("%a %b %d %H:%M:%S MET %Y", gmtime()), message.getText())) content = "".join(dbxBuffer) dbxStream.close() return content def OEIdentityKeys(): """Return the OE identity keys. Tested with Outlook Express 6.0 with Windows XP.""" if win32api is None: # Delayed import error from top. raise ImportError("pywin32 not installed") reg = win32api.RegOpenKeyEx(win32con.HKEY_USERS, "") user_index = 0 while True: # Loop through all the users try: user_name = "%s\\Identities" % \ (win32api.RegEnumKey(reg, user_index),) except win32api.error: break user_index += 1 try: user_key = win32api.RegOpenKeyEx(win32con.HKEY_USERS, user_name) except win32api.error: # Not this one continue identity_index = 0 while True: # Loop through all the identities try: identity_name = win32api.RegEnumKey(user_key, identity_index) except win32api.error: break identity_index += 1 subkey_name = "%s\\%s\\%s" % (user_name, identity_name, "Software\\Microsoft\\Outlook " \ "Express\\5.0") try: subkey = win32api.RegOpenKeyEx(win32con.HKEY_USERS, subkey_name, 0, win32con.KEY_READ) except win32api.error: # Not this user continue yield subkey def OECurrentUserKey(): """Returns the root registry key for current user Outlook Express settings.""" if win32api is None: # Delayed import error from top. raise ImportError("pywin32 not installed") key = "Identities" reg = win32api.RegOpenKeyEx(win32con.HKEY_CURRENT_USER, key) id = win32api.RegQueryValueEx(reg, "Default User ID")[0] subKey = "%s\\%s\\Software\\Microsoft\\Outlook Express\\5.0" % (key, id) return subKey def OEStoreRoot(): """Return the path to the Outlook Express Store Root. Tested with Outlook Express 6.0 with Windows XP.""" subKey = OECurrentUserKey() reg = win32api.RegOpenKeyEx(win32con.HKEY_CURRENT_USER, subKey) path = win32api.RegQueryValueEx(reg, "Store Root")[0] # I can't find a shellcon to that is the same as %UserProfile%, # so extract it from CSIDL_LOCAL_APPDATA UserDirectory = shell.SHGetFolderPath \ (0, shellcon.CSIDL_LOCAL_APPDATA, 0, 0) parts = UserDirectory.split(os.sep) UserProfile = os.sep.join(parts[:-2]) return path.replace("%UserProfile%", UserProfile) def OEDBXFilesList(): """Returns a list of DBX files for current user.""" path = OEStoreRoot() dbx_re = re.compile('.+\.dbx') dbxs = [f for f in os.listdir(path) if dbx_re.search(f) != None] return dbxs def OEAccountKeys(permission = None): """Return registry keys for each of the OE mail accounts, along with information about what type of mail account it is.""" if permission is None: # Can't do this in the parameter, because then it requires # win32con to be available for the module to be imported. permission = win32con.KEY_READ | win32con.KEY_SET_VALUE possible_root_keys = [] # This appears to be the place for OE6 and WinXP # (So I'm guessing also for NT4) if sys.getwindowsversion()[0] >= 4: possible_root_keys = ["Software\\Microsoft\\" \ "Internet Account Manager\\Accounts"] else: # This appears to be the place for OE6 and Win98 # (So I'm guessing also for Win95) possible_root_keys = OEIdentityKeys() for key in possible_root_keys: reg = win32api.RegOpenKeyEx(win32con.HKEY_CURRENT_USER, key) account_index = 0 while True: # Loop through all the accounts account = {} try: subkey_name = "%s\\%s" % \ (key, win32api.RegEnumKey(reg, account_index)) except win32api.error: break account_index += 1 index = 0 subkey = win32api.RegOpenKeyEx(win32con.HKEY_CURRENT_USER, subkey_name, 0, permission) while True: # Loop through all the keys so that we can determine # what type of account this is. try: name, value, typ = win32api.RegEnumValue(subkey, index) except win32api.error: break account[name] = (value, typ) index += 1 # Yield, as appropriate. if account.has_key("POP3 Server"): yield("POP3", subkey, account) elif account.has_key("IMAP Server"): yield("IMAP4", subkey, account) def OEIsInstalled(): """Return True if Outlook Express appears to be installed, and in use (I think if sys.platform == "win32" would say if it was installed at all).""" # Our heuristic is that there is at least one mail account setup. if len(list(OEAccountKeys)) > 0: return True return False ## For use by the test tools. class OEMsg(msgs.Msg): def __init__(self, guts, id): self.tag = id self.guts = guts # The iterator yields a stream of Msg objects, taken from a list of # dbx files. class OEMsgStream(msgs.MsgStream): def __init__(self, tag, dbxes, keep=None): msgs.MsgStream.__init__(self, tag, dbxes, keep) def produce(self): if self.keep is None: for dbx in self.directories: folder = convertToMbox(file(dbx)) all = folder.split("\nFrom ") # XXX Is this right? count = 0 for msg in all: id = "%s::%s" % (dbx, count) count += 1 yield OEMsg(msg, id) return # We only want part of the msgs. Shuffle each directory list, but # in such a way that we'll get the same result each time this is # called on the same directory list. for directory in self.directories: folder = convertToMbox(file(dbx)) all = folder.split("\nFrom ") # XXX Is this right? random.seed(hash(max(all)) ^ SEED) # reproducible across calls random.shuffle(all) del all[self.keep:] all.sort() # for consistency with MsgStream count = 0 for msg in all: id = "%s::%s" % (dbx, count) count += 1 yield OEMsg(msg, id) class OEHamStream(msgs.HamStream): def __init__(self, tag, dbxes, train=0): msgs.HamStream.__init__(self, tag, dbxes, train) class OESpamStream(msgs.SpamStream): def __init__(self, tag, dbxes, train=0): msgs.SpamStream.__init__(self, tag, dbxes, train) ########################################################################### ## TEST DRIVER ########################################################################### def test(): import getopt try: opts, args = getopt.getopt(sys.argv[1:], 'hp') except getopt.error, msg: print >> sys.stderr, str(msg) + '\n\n' + __doc__ sys.exit() print_message = False for opt, arg in opts: if opt == '-h': print >> sys.stderr, __doc__ sys.exit() elif opt == '-p': print_message = True MAILBOX_DIR = OEStoreRoot() files = [os.path.join(MAILBOX_DIR, f) for f in OEDBXFilesList()] for file in files: try: print print file dbx = open(file, "rb", 0) header = dbxFileHeader(dbx) print "IS VALID DBX :", header.isValid() if header.isMessages(): info = dbxFileInfo(dbx, header.getEntry(dbxFileHeader.FH_FILE_INFO_LENGTH)) print "MAILBOX NAME :", info.getFolderName() print "CREATION TIME :", info.getCreationTime() entries = header.getEntry(dbxFileHeader.FH_ENTRIES) address = header.getEntry(dbxFileHeader.FH_TREE_ROOT_NODE_PTR) if address and entries: tree = dbxTree(dbx, address, entries) for i in range(entries): address = tree.getValue(i) messageInfo = dbxMessageInfo(dbx, address) if messageInfo.isIndexed(dbxMessageInfo.MI_MESSAGE_ADDRESS): messageAddress = messageInfo.getValueAsLong(dbxMessageInfo.MI_MESSAGE_ADDRESS) message = dbxMessage(dbx, messageAddress) if print_message: print print "Message :", messageInfo.getString(dbxMessageInfo.MI_SUBJECT) print "=" * (len(messageInfo.getString(dbxMessageInfo.MI_SUBJECT)) + 9) print print message.getText() dbx.close() except Exception, (strerror): print strerror if __name__ == '__main__': test() spambayes-1.1a6/spambayes/optimize.py0000644000076500000240000000441211137577265020074 0ustar skipstaff00000000000000# __version__ = '$Id: optimize.py 3232 2009-01-27 12:31:49Z montanaro $' # # Optimize any parametric function. # import copy def SimplexMaximize(var, err, func, convcrit = 0.001, minerr = 0.001): import numpy var = numpy.array(var) simplex = [var] for i in range(len(var)): var2 = copy.copy(var) var2[i] = var[i] + err[i] simplex.append(var2) value = [] for i in range(len(simplex)): value.append(func(simplex[i])) while 1: # Determine worst and best wi = 0 bi = 0 for i in range(len(simplex)): if value[wi] > value[i]: wi = i if value[bi] < value[i]: bi = i # Test for convergence #print "worst, best are",wi,bi,"with",value[wi],value[bi] if abs(value[bi] - value[wi]) <= convcrit: return simplex[bi] # Calculate average of non-worst ave = numpy.zeros(len(var), dtype=numpy.float) for i in range(len(simplex)): if i != wi: ave = ave + simplex[i] ave = ave / (len(simplex) - 1) worst = numpy.array(simplex[wi]) # Check for too-small simplex simsize = numpy.add.reduce(numpy.absolute(ave - worst)) if simsize <= minerr: #print "Size of simplex too small:",simsize return simplex[bi] # Invert worst new = 2 * ave - simplex[wi] newv = func(new) if newv <= value[wi]: # Even worse. Shrink instead #print "Shrunk simplex" #print "ave=",repr(ave) #print "wi=",repr(worst) new = 0.5 * ave + 0.5 * worst newv = func(new) elif newv > value[bi]: # Better than the best. Expand new2 = 3 * ave - 2 * worst newv2 = func(new2) if newv2 > newv: # Accept #print "Expanded simplex" new = new2 newv = newv2 simplex[wi] = new value[wi] = newv def DoubleSimplexMaximize(var, err, func, convcrit=0.001, minerr=0.001): import numpy err = numpy.array(err) var = SimplexMaximize(var, err, func, convcrit*5, minerr*5) return SimplexMaximize(var, 0.4 * err, func, convcrit, minerr) spambayes-1.1a6/spambayes/Options.py0000664000076500000240000020206311355064504017660 0ustar skipstaff00000000000000"""Options Abstract: Options.options is a globally shared options object. This object is initialised when the module is loaded: the envar BAYESCUSTOMIZE is checked for a list of names, if nothing is found then the local directory and the home directory are checked for a file called bayescustomize.ini or .spambayesrc (respectively) and the initial values are loaded from this. The Option class is defined in OptionsClass.py - this module is responsible only for instantiating and loading the globally shared instance. To Do: o Suggestions? """ import sys, os try: _ except NameError: _ = lambda arg: arg __all__ = ['options', '_'] # Grab the stuff from the core options class. from spambayes.OptionsClass import * # A little magic. We'd like to use ZODB as the default storage, # because we've had so many problems with bsddb, and we'd like to swap # to new ZODB problems . However, apart from this, we only need # a standard Python install - if the default was ZODB then we would # need ZODB to be installed as well (which it will br for binary users, # but might not be for source users). So what we do is check whether # ZODB is importable and if it is, default to that, and if not, default # to dbm. If ZODB is sometimes importable and sometimes not (e.g. you # muck around with the PYTHONPATH), then this may not work well - the # best idea would be to explicitly put the type in your configuration # file. try: import ZODB except ImportError: DB_TYPE = "dbm", "hammie.db", "spambayes.messageinfo.db" else: del ZODB DB_TYPE = "zodb", "hammie.fs", "messageinfo.fs" # Format: # defaults is a dictionary, where the keys are the section names # each key maps to a tuple consisting of: # option name, display name, default, # doc string, possible values, restore on restore-to-defaults # The display name and doc string should be enclosed in _() to allow # i18n. In a few cases, then possible values should also be enclosed # in _(). defaults = { "Tokenizer" : ( ("basic_header_tokenize", _("Basic header tokenising"), False, _("""If true, tokenizer.Tokenizer.tokenize_headers() will tokenize the contents of each header field just like the text of the message body, using the name of the header as a tag. Tokens look like "header:word". The basic approach is simple and effective, but also very sensitive to biases in the ham and spam collections. For example, if the ham and spam were collected at different times, several headers with date/time information will become the best discriminators. (Not just Date, but Received and X-From_.)"""), BOOLEAN, RESTORE), ("basic_header_tokenize_only", _("Only basic header tokenising"), False, _("""If true and basic_header_tokenize is also true, then basic_header_tokenize is the only action performed."""), BOOLEAN, RESTORE), ("basic_header_skip", _("Basic headers to skip"), ("received date x-.*",), _("""If basic_header_tokenize is true, then basic_header_skip is a set of headers that should be skipped."""), HEADER_NAME, RESTORE), ("check_octets", _("Check application/octet-stream sections"), False, _("""If true, the first few characters of application/octet-stream sections are used, undecoded. What 'few' means is decided by octet_prefix_size."""), BOOLEAN, RESTORE), ("octet_prefix_size", _("Number of characters of octet stream to process"), 5, _("""The number of characters of the application/octet-stream sections to use, if check_octets is set to true."""), INTEGER, RESTORE), ("x-short_runs", _("Count runs of short 'words'"), False, _("""(EXPERIMENTAL) If true, generate tokens based on max number of short word runs. Short words are anything of length < the skip_max_word_size option. Normally they are skipped, but one common spam technique spells words like 'V I A G RA'. """), BOOLEAN, RESTORE), ("x-lookup_ip", _("Generate IP address tokens from hostnames"), False, _("""(EXPERIMENTAL) Generate IP address tokens from hostnames. Requires PyDNS (http://pydns.sourceforge.net/)."""), BOOLEAN, RESTORE), ("lookup_ip_cache", _("x-lookup_ip cache file location"), "", _("""Tell SpamBayes where to cache IP address lookup information. Only comes into play if lookup_ip is enabled. The default (empty string) disables the file cache. When caching is enabled, the cache file is stored using the same database type as the main token store (only dbm and zodb supported so far, zodb has problems, dbm is untested, hence the default)."""), PATH, RESTORE), ("image_size", _("Generate image size tokens"), False, _("""If true, generate tokens based on the sizes of embedded images."""), BOOLEAN, RESTORE), ("crack_images", _("Look inside images for text"), False, _("""If true, generate tokens based on the (hopefully) text content contained in any images in each message. The current support is minimal, relies on the installation of an OCR 'engine' (see ocr_engine.)"""), BOOLEAN, RESTORE), ("ocr_engine", _("OCR engine to use"), "", _("""The name of the OCR engine to use. If empty, all supported engines will be checked to see if they are installed. Engines currently supported include ocrad (http://www.gnu.org/software/ocrad/ocrad.html) and gocr (http://jocr.sourceforge.net/download.html) and they require the appropriate executable be installed in either your PATH, or in the main spambayes directory."""), HEADER_VALUE, RESTORE), ("crack_image_cache", _("Cache to speed up ocr."), "", _("""If non-empty, names a file from which to read cached ocr info at start and to which to save that info at exit."""), PATH, RESTORE), ("ocrad_scale", _("Scale factor to use with ocrad."), 2, _("""Specifies the scale factor to apply when running ocrad. While you can specify a negative scale it probably won't help. Scaling up by a factor of 2 or 3 seems to work well for the sort of spam images encountered by SpamBayes."""), INTEGER, RESTORE), ("ocrad_charset", _("Charset to apply with ocrad."), "ascii", _("""Specifies the charset to use when running ocrad. Valid values are 'ascii', 'iso-8859-9' and 'iso-8859-15'."""), OCRAD_CHARSET, RESTORE), ("max_image_size", _("Max image size to try OCR-ing"), 100000, _("""When crack_images is enabled, this specifies the largest image to try OCR on."""), INTEGER, RESTORE), ("count_all_header_lines", _("Count all header lines"), False, _("""Generate tokens just counting the number of instances of each kind of header line, in a case-sensitive way. Depending on data collection, some headers are not safe to count. For example, if ham is collected from a mailing list but spam from your regular inbox traffic, the presence of a header like List-Info will be a very strong ham clue, but a bogus one. In that case, set count_all_header_lines to False, and adjust safe_headers instead."""), BOOLEAN, RESTORE), ("record_header_absence", _("Record header absence"), False, _("""When True, generate a "noheader:HEADERNAME" token for each header in safe_headers (below) that *doesn't* appear in the headers. This helped in various of Tim's python.org tests, but appeared to hurt a little in Anthony Baxter's tests."""), BOOLEAN, RESTORE), ("safe_headers", _("Safe headers"), ("abuse-reports-to", "date", "errors-to", "from", "importance", "in-reply-to", "message-id", "mime-version", "organization", "received", "reply-to", "return-path", "subject", "to", "user-agent", "x-abuse-info", "x-complaints-to", "x-face"), _("""Like count_all_header_lines, but restricted to headers in this list. safe_headers is ignored when count_all_header_lines is true, unless record_header_absence is also true."""), HEADER_NAME, RESTORE), ("mine_received_headers", _("Mine the received headers"), False, _("""A lot of clues can be gotten from IP addresses and names in Received: headers. This can give spectacular results for bogus reasons if your corpora are from different sources."""), BOOLEAN, RESTORE), ("x-mine_nntp_headers", _("Mine NNTP-Posting-Host headers"), False, _("""Usenet is host to a lot of spam. Usenet/Mailing list gateways can let it leak across. Similar to mining received headers, we pick apart the IP address or host name in this header for clues."""), BOOLEAN, RESTORE), ("address_headers", _("Address headers to mine"), ("from", "to", "cc", "sender", "reply-to"), _("""Mine the following address headers. If you have mixed source corpuses (as opposed to a mixed sauce walrus, which is delicious!) then you probably don't want to use 'to' or 'cc') Address headers will be decoded, and will generate charset tokens as well as the real address. Others to consider: errors-to, ..."""), HEADER_NAME, RESTORE), ("generate_long_skips", _("Generate long skips"), True, _("""If legitimate mail contains things that look like text to the tokenizer and turning turning off this option helps (perhaps binary attachments get 'defanged' by something upstream from this operation and thus look like text), this may help, and should be an alert that perhaps the tokenizer is broken."""), BOOLEAN, RESTORE), ("summarize_email_prefixes", _("Summarise email prefixes"), False, _("""Try to capitalize on mail sent to multiple similar addresses."""), BOOLEAN, RESTORE), ("summarize_email_suffixes", _("Summarise email suffixes"), False, _("""Try to capitalize on mail sent to multiple similar addresses."""), BOOLEAN, RESTORE), ("skip_max_word_size", _("Long skip trigger length"), 12, _("""Length of words that triggers 'long skips'. Longer than this triggers a skip."""), INTEGER, RESTORE), ("x-pick_apart_urls", _("Extract clues about url structure"), False, _("""(EXPERIMENTAL) Note whether url contains non-standard port or user/password elements."""), BOOLEAN, RESTORE), ("x-fancy_url_recognition", _("Extract URLs without http:// prefix"), False, _("""(EXPERIMENTAL) Recognize 'www.python.org' or ftp.python.org as URLs instead of just long words."""), BOOLEAN, RESTORE), ("replace_nonascii_chars", _("Replace non-ascii characters"), False, _("""If true, replace high-bit characters (ord(c) >= 128) and control characters with question marks. This allows non-ASCII character strings to be identified with little training and small database burden. It's appropriate only if your ham is plain 7-bit ASCII, or nearly so, so that the mere presence of non-ASCII character strings is known in advance to be a strong spam indicator."""), BOOLEAN, RESTORE), ("x-search_for_habeas_headers", _("Search for Habeas Headers"), False, _("""(EXPERIMENTAL) If true, search for the habeas headers (see http://www.habeas.com). If they are present and correct, this should be a strong ham sign, if they are present and incorrect, this should be a strong spam sign."""), BOOLEAN, RESTORE), ("x-reduce_habeas_headers", _("Reduce Habeas Header Tokens to Single"), False, _("""(EXPERIMENTAL) If SpamBayes is set to search for the Habeas headers, nine tokens are generated for messages with habeas headers. This should be fine, since messages with the headers should either be ham, or result in FN so that we can send them to habeas so they can be sued. However, to reduce the strength of habeas headers, we offer the ability to reduce the nine tokens to one. (This option has no effect if 'Search for Habeas Headers' is False)"""), BOOLEAN, RESTORE), ), # These options are all experimental; it seemed better to put them into # their own category than have several interdependant experimental options. # If this capability is removed, the entire section can go. "URLRetriever" : ( ("x-slurp_urls", _("Tokenize text content at the end of URLs"), False, _("""(EXPERIMENTAL) If this option is enabled, when a message normally scores in the 'unsure' range, and has fewer tokens than the maximum looked at, and contains URLs, then the text at those URLs is obtained and tokenized. If those tokens result in the message moving to a score outside the 'unsure' range, then they are added to the tokens for the message. This should be particularly effective for messages that contain only a single URL and no other text."""), BOOLEAN, RESTORE), ("x-cache_expiry_days", _("Number of days to store URLs in cache"), 7, _("""(EXPERIMENTAL) This is the number of days that local cached copies of the text at the URLs will be stored for."""), INTEGER, RESTORE), ("x-cache_directory", _("URL Cache Directory"), "url-cache", _("""(EXPERIMENTAL) So that SpamBayes doesn't need to retrieve the same URL over and over again, it stores local copies of the text at the end of the URL. This is the directory that will be used for those copies."""), PATH, RESTORE), ("x-only_slurp_base", _("Retrieve base url"), False, _("""(EXPERIMENTAL) To try and speed things up, and to avoid following unique URLS, if this option is enabled, SpamBayes will convert the URL to as basic a form it we can. All directory information is removed and the domain is reduced to the two (or three for those with a country TLD) top-most elements. For example, http://www.massey.ac.nz/~tameyer/index.html?you=me would become http://massey.ac.nz and http://id.example.com would become http://example.com This should have two beneficial effects: o It's unlikely that any information could be contained in this 'base' url that could identify the user (unless they have a *lot* of domains). o Many urls (both spam and ham) will strip down into the same 'base' url. Since we have a limited form of caching, this means that a lot fewer urls will have to be retrieved. However, this does mean that if the 'base' url is hammy and the full is spammy, or vice-versa, that the slurp will give back the wrong information. Whether or not this is the case would have to be determined by testing. """), BOOLEAN, RESTORE), ("x-web_prefix", _("Prefix for tokens from web pages"), "", _("""(EXPERIMENTAL) It may be that what is hammy/spammy for you in email isn't from webpages. You can then set this option (to "web:", for example), and effectively create an independent (sub)database for tokens derived from parsing web pages."""), r"[\S]+", RESTORE), ), # These options control how a message is categorized "Categorization" : ( # spam_cutoff and ham_cutoff are used in Python slice sense: # A msg is considered ham if its score is in 0:ham_cutoff # A msg is considered unsure if its score is in ham_cutoff:spam_cutoff # A msg is considered spam if its score is in spam_cutoff: # # So it's unsure iff ham_cutoff <= score < spam_cutoff. # For a binary classifier, make ham_cutoff == spam_cutoff. # ham_cutoff > spam_cutoff doesn't make sense. # # The defaults here (.2 and .9) may be appropriate for the default chi- # combining scheme. Cutoffs for chi-combining typically aren't touchy, # provided you're willing to settle for "really good" instead of "optimal". # Tim found that .3 and .8 worked very well for well-trained systems on # his personal email, and his large comp.lang.python test. If just # beginning training, or extremely fearful of mistakes, 0.05 and 0.95 may # be more appropriate for you. # # Picking good values for gary-combining is much harder, and appears to be # corpus-dependent, and within a single corpus dependent on how much # training has been done. Values from 0.50 thru the low 0.60's have been # reported to work best by various testers on their data. ("ham_cutoff", _("Ham cutoff"), 0.20, _("""Spambayes gives each email message a spam probability between 0 and 1. Emails below the Ham Cutoff probability are classified as Ham. Larger values will result in more messages being classified as ham, but with less certainty that all of them actually are ham. This value should be between 0 and 1, and should be smaller than the Spam Cutoff."""), REAL, RESTORE), ("spam_cutoff", _("Spam cutoff"), 0.90, _("""Emails with a spam probability above the Spam Cutoff are classified as Spam - just like the Ham Cutoff but at the other end of the scale. Messages that fall between the two values are classified as Unsure."""), REAL, RESTORE), ), # These control various displays in class TestDriver.Driver, and # Tester.Test. "TestDriver" : ( ("nbuckets", _("Number of buckets"), 200, _("""Number of buckets in histograms."""), INTEGER, RESTORE), ("show_histograms", _("Show histograms"), True, _(""""""), BOOLEAN, RESTORE), ("compute_best_cutoffs_from_histograms", _("Compute best cutoffs from histograms"), True, _("""After the display of a ham+spam histogram pair, you can get a listing of all the cutoff values (coinciding with histogram bucket boundaries) that minimize: best_cutoff_fp_weight * (# false positives) + best_cutoff_fn_weight * (# false negatives) + best_cutoff_unsure_weight * (# unsure msgs) This displays two cutoffs: hamc and spamc, where 0.0 <= hamc <= spamc <= 1.0 The idea is that if something scores < hamc, it's called ham; if something scores >= spamc, it's called spam; and everything else is called 'I am not sure' -- the middle ground. Note: You may wish to increase nbuckets, to give this scheme more cutoff values to analyze."""), BOOLEAN, RESTORE), ("best_cutoff_fp_weight", _("Best cutoff false positive weight"), 10.00, _(""""""), REAL, RESTORE), ("best_cutoff_fn_weight", _("Best cutoff false negative weight"), 1.00, _(""""""), REAL, RESTORE), ("best_cutoff_unsure_weight", _("Best cutoff unsure weight"), 0.20, _(""""""), REAL, RESTORE), ("percentiles", _("Percentiles"), (5, 25, 75, 95), _("""Histogram analysis also displays percentiles. For each percentile p in the list, the score S such that p% of all scores are <= S is given. Note that percentile 50 is the median, and is displayed (along with the min score and max score) independent of this option."""), INTEGER, RESTORE), ("show_spam_lo", _(""), 1.0, _("""Display spam when show_spam_lo <= spamprob <= show_spam_hi and likewise for ham. The defaults here do not show anything."""), REAL, RESTORE), ("show_spam_hi", _(""), 0.0, _("""Display spam when show_spam_lo <= spamprob <= show_spam_hi and likewise for ham. The defaults here do not show anything."""), REAL, RESTORE), ("show_ham_lo", _(""), 1.0, _("""Display spam when show_spam_lo <= spamprob <= show_spam_hi and likewise for ham. The defaults here do not show anything."""), REAL, RESTORE), ("show_ham_hi", _(""), 0.0, _("""Display spam when show_spam_lo <= spamprob <= show_spam_hi and likewise for ham. The defaults here do not show anything."""), REAL, RESTORE), ("show_false_positives", _("Show false positives"), True, _(""""""), BOOLEAN, RESTORE), ("show_false_negatives", _("Show false negatives"), False, _(""""""), BOOLEAN, RESTORE), ("show_unsure", _("Show unsure"), False, _(""""""), BOOLEAN, RESTORE), ("show_charlimit", _("Show character limit"), 3000, _("""The maximum # of characters to display for a msg displayed due to the show_xyz options above."""), INTEGER, RESTORE), ("save_trained_pickles", _("Save trained pickles"), False, _("""If save_trained_pickles is true, Driver.train() saves a binary pickle of the classifier after training. The file basename is given by pickle_basename, the extension is .pik, and increasing integers are appended to pickle_basename. By default (if save_trained_pickles is true), the filenames are class1.pik, class2.pik, ... If a file of that name already exists, it is overwritten. pickle_basename is ignored when save_trained_pickles is false."""), BOOLEAN, RESTORE), ("pickle_basename", _("Pickle basename"), "class", _(""""""), r"[\w]+", RESTORE), ("save_histogram_pickles", _("Save histogram pickles"), False, _("""If save_histogram_pickles is true, Driver.train() saves a binary pickle of the spam and ham histogram for "all test runs". The file basename is given by pickle_basename, the suffix _spamhist.pik or _hamhist.pik is appended to the basename."""), BOOLEAN, RESTORE), ("spam_directories", _("Spam directories"), "Data/Spam/Set%d", _("""default locations for timcv and timtest - these get the set number interpolated."""), VARIABLE_PATH, RESTORE), ("ham_directories", _("Ham directories"), "Data/Ham/Set%d", _("""default locations for timcv and timtest - these get the set number interpolated."""), VARIABLE_PATH, RESTORE), ), "CV Driver": ( ("build_each_classifier_from_scratch", _("Build each classifier from scratch"), False, _("""A cross-validation driver takes N ham+spam sets, and builds N classifiers, training each on N-1 sets, and the predicting against the set not trained on. By default, it does this in a clever way, learning *and* unlearning sets as it goes along, so that it never needs to train on N-1 sets in one gulp after the first time. Setting this option true forces ''one gulp from-scratch'' training every time. There used to be a set of combining schemes that needed this, but now it is just in case you are paranoid ."""), BOOLEAN, RESTORE), ), "Classifier": ( ("max_discriminators", _("Maximum number of extreme words"), 150, _("""The maximum number of extreme words to look at in a message, where "extreme" means with spam probability farthest away from 0.5. 150 appears to work well across all corpora tested."""), INTEGER, RESTORE), ("unknown_word_prob", _("Unknown word probability"), 0.5, _("""These two control the prior assumption about word probabilities. unknown_word_prob is essentially the probability given to a word that has never been seen before. Nobody has reported an improvement via moving it away from 1/2, although Tim has measured a mean spamprob of a bit over 0.5 (0.51-0.55) in 3 well-trained classifiers."""), REAL, RESTORE), ("unknown_word_strength", _("Unknown word strength"), 0.45, _("""This adjusts how much weight to give the prior assumption relative to the probabilities estimated by counting. At 0, the counting estimates are believed 100%, even to the extent of assigning certainty (0 or 1) to a word that has appeared in only ham or only spam. This is a disaster. As unknown_word_strength tends toward infinity, all probabilities tend toward unknown_word_prob. All reports were that a value near 0.4 worked best, so this does not seem to be corpus-dependent."""), REAL, RESTORE), ("minimum_prob_strength", _("Minimum probability strength"), 0.1, _("""When scoring a message, ignore all words with abs(word.spamprob - 0.5) < minimum_prob_strength. This may be a hack, but it has proved to reduce error rates in many tests. 0.1 appeared to work well across all corpora."""), REAL, RESTORE), ("use_chi_squared_combining", _("Use chi-squared combining"), True, _("""For vectors of random, uniformly distributed probabilities, -2*sum(ln(p_i)) follows the chi-squared distribution with 2*n degrees of freedom. This is the "provably most-sensitive" test the original scheme was monotonic with. Getting closer to the theoretical basis appears to give an excellent combining method, usually very extreme in its judgment, yet finding a tiny (in # of msgs, spread across a huge range of scores) middle ground where lots of the mistakes live. This is the best method so far. One systematic benefit is is immunity to "cancellation disease". One systematic drawback is sensitivity to *any* deviation from a uniform distribution, regardless of whether actually evidence of ham or spam. Rob Hooft alleviated that by combining the final S and H measures via (S-H+1)/2 instead of via S/(S+H)). In practice, it appears that setting ham_cutoff=0.05, and spam_cutoff=0.95, does well across test sets; while these cutoffs are rarely optimal, they get close to optimal. With more training data, Tim has had good luck with ham_cutoff=0.30 and spam_cutoff=0.80 across three test data sets (original c.l.p data, his own email, and newer general python.org traffic)."""), BOOLEAN, RESTORE), ("use_bigrams", _("Use mixed uni/bi-grams scheme"), False, _("""Generate both unigrams (words) and bigrams (pairs of words). However, extending an idea originally from Gary Robinson, the message is 'tiled' into non-overlapping unigrams and bigrams, approximating the strongest outcome over all possible tilings. Note that to really test this option you need to retrain with it on, so that your database includes the bigrams - if you subsequently turn it off, these tokens will have no effect. This option will at least double your database size given the same training data, and will probably at least triple it. You may also wish to increase the max_discriminators (maximum number of extreme words) option if you enable this option, perhaps doubling or quadrupling it. It's not yet clear. Bigrams create many more hapaxes, and that seems to increase the brittleness of minimalist training regimes; increasing max_discriminators may help to soften that effect. OTOH, max_discriminators defaults to 150 in part because that makes it easy to prove that the chi-squared math is immune from numeric problems. Increase it too much, and insane results will eventually result (including fatal floating-point exceptions on some boxes). This option is experimental, and may be removed in a future release. We would appreciate feedback about it if you use it - email spambayes@python.org with your comments and results. """), BOOLEAN, RESTORE), ), "Hammie": ( ("train_on_filter", _("Train when filtering"), False, _("""Train when filtering? After filtering a message, hammie can then train itself on the judgement (ham or spam). This can speed things up with a procmail-based solution. If you do enable this, please make sure to retrain any mistakes. Otherwise, your word database will slowly become useless. Note that this option is only used by sb_filter, and will have no effect on sb_server's POP3 proxy, or the IMAP filter."""), BOOLEAN, RESTORE), ), # These options control where Spambayes data will be stored, and in # what form. They are used by many Spambayes applications (including # pop3proxy, smtpproxy, imapfilter and hammie), and mean that data # (such as the message database) is shared between the applications. # If this is not the desired behaviour, you must have a different # value for each of these options in a configuration file that gets # loaded by the appropriate application only. "Storage" : ( ("persistent_use_database", _("Database backend"), DB_TYPE[0], _("""SpamBayes can use either a ZODB or dbm database (quick to score one message) or a pickle (quick to train on huge amounts of messages). There is also (experimental) ability to use a mySQL or PostgresSQL database."""), ("zeo", "zodb", "cdb", "mysql", "pgsql", "dbm", "pickle"), RESTORE), ("persistent_storage_file", _("Storage file name"), DB_TYPE[1], _("""Spambayes builds a database of information that it gathers from incoming emails and from you, the user, to get better and better at classifying your email. This option specifies the name of the database file. If you don't give a full pathname, the name will be taken to be relative to the location of the most recent configuration file loaded."""), FILE_WITH_PATH, DO_NOT_RESTORE), ("messageinfo_storage_file", _("Message information file name"), DB_TYPE[2], _("""Spambayes builds a database of information about messages that it has already seen and trained or classified. This database is used to ensure that these messages are not retrained or reclassified (unless specifically requested to). This option specifies the name of the database file. If you don't give a full pathname, the name will be taken to be relative to the location of the most recent configuration file loaded."""), FILE_WITH_PATH, DO_NOT_RESTORE), ("cache_use_gzip", _("Use gzip"), False, _("""Use gzip to compress the cache."""), BOOLEAN, RESTORE), ("cache_expiry_days", _("Days before cached messages expire"), 7, _("""Messages will be expired from the cache after this many days. After this time, you will no longer be able to train on these messages (note this does not affect the copy of the message that you have in your mail client)."""), INTEGER, RESTORE), ("spam_cache", _("Spam cache directory"), "pop3proxy-spam-cache", _("""Directory that SpamBayes should cache spam in. If this does not exist, it will be created."""), PATH, DO_NOT_RESTORE), ("ham_cache", _("Ham cache directory"), "pop3proxy-ham-cache", _("""Directory that SpamBayes should cache ham in. If this does not exist, it will be created."""), PATH, DO_NOT_RESTORE), ("unknown_cache", _("Unknown cache directory"), "pop3proxy-unknown-cache", _("""Directory that SpamBayes should cache unclassified messages in. If this does not exist, it will be created."""), PATH, DO_NOT_RESTORE), ("core_spam_cache", _("Spam cache directory"), "core-spam-cache", _("""Directory that SpamBayes should cache spam in. If this does not exist, it will be created."""), PATH, DO_NOT_RESTORE), ("core_ham_cache", _("Ham cache directory"), "core-ham-cache", _("""Directory that SpamBayes should cache ham in. If this does not exist, it will be created."""), PATH, DO_NOT_RESTORE), ("core_unknown_cache", _("Unknown cache directory"), "core-unknown-cache", _("""Directory that SpamBayes should cache unclassified messages in. If this does not exist, it will be created."""), PATH, DO_NOT_RESTORE), ("cache_messages", _("Cache messages"), True, _("""You can disable the pop3proxy caching of messages. This will make the proxy a bit faster, and make it use less space on your hard drive. The proxy uses its cache for reviewing and training of messages, so if you disable caching you won't be able to do further training unless you re-enable it. Thus, you should only turn caching off when you are satisfied with the filtering that Spambayes is doing for you."""), BOOLEAN, RESTORE), ("no_cache_bulk_ham", _("Suppress caching of bulk ham"), False, _("""Where message caching is enabled, this option suppresses caching of messages which are classified as ham and marked as 'Precedence: bulk' or 'Precedence: list'. If you subscribe to a high-volume mailing list then your 'Review messages' page can be overwhelmed with list messages, making training a pain. Once you've trained Spambayes on enough list traffic, you can use this option to prevent that traffic showing up in 'Review messages'."""), BOOLEAN, RESTORE), ("no_cache_large_messages", _("Maximum size of cached messages"), 0, _("""Where message caching is enabled, this option suppresses caching of messages which are larger than this value (measured in bytes). If you receive a lot of messages that include large attachments (and are correctly classified), you may not wish to cache these. If you set this to zero (0), then this option will have no effect."""), INTEGER, RESTORE), ), # These options control the various headers that some Spambayes # applications add to incoming mail, including imapfilter, pop3proxy, # and hammie. "Headers" : ( # The name of the header that hammie, pop3proxy, and any other spambayes # software, adds to emails in filter mode. This will definately contain # the "classification" of the mail, and may also (i.e. with hammie) # contain the score ("classification_header_name", _("Classification header name"), "X-Spambayes-Classification", _("""Spambayes classifies each message by inserting a new header into the message. This header can then be used by your email client (provided your client supports filtering) to move spam into a separate folder (recommended), delete it (not recommended), etc. This option specifies the name of the header that Spambayes inserts. The default value should work just fine, but you may change it to anything that you wish."""), HEADER_NAME, RESTORE), # The three disposition names are added to the header as the following # three words: ("header_spam_string", _("Spam disposition name"), _("spam"), _("""The header that Spambayes inserts into each email has a name, (Classification header name, above), and a value. If the classifier determines that this email is probably spam, it places a header named as above with a value as specified by this string. The default value should work just fine, but you may change it to anything that you wish."""), HEADER_VALUE, RESTORE), ("header_ham_string", _("Ham disposition name"), _("ham"), _("""As for Spam Designation, but for emails classified as Ham."""), HEADER_VALUE, RESTORE), ("header_unsure_string", _("Unsure disposition name"), _("unsure"), _("""As for Spam/Ham Designation, but for emails which the classifer wasn't sure about (ie. the spam probability fell between the Ham and Spam Cutoffs). Emails that have this classification should always be the subject of training."""), HEADER_VALUE, RESTORE), ("header_score_digits", _("Accuracy of reported score"), 2, _("""Accuracy of the score in the header in decimal digits."""), INTEGER, RESTORE), ("header_score_logarithm", _("Augment score with logarithm"), False, _("""Set this option to augment scores of 1.00 or 0.00 by a logarithmic "one-ness" or "zero-ness" score (basically it shows the "number of zeros" or "number of nines" next to the score value)."""), BOOLEAN, RESTORE), ("include_score", _("Add probability (score) header"), False, _("""You can have Spambayes insert a header with the calculated spam probability into each mail. If you can view headers with your mailer, then you can see this information, which can be interesting and even instructive if you're a serious SpamBayes junkie."""), BOOLEAN, RESTORE), ("score_header_name", _("Probability (score) header name"), "X-Spambayes-Spam-Probability", _(""""""), HEADER_NAME, RESTORE), ("include_thermostat", _("Add level header"), False, _("""You can have spambayes insert a header with the calculated spam probability, expressed as a number of '*'s, into each mail (the more '*'s, the higher the probability it is spam). If your mailer supports it, you can use this information to fine tune your classification of ham/spam, ignoring the classification given."""), BOOLEAN, RESTORE), ("thermostat_header_name", _("Level header name"), "X-Spambayes-Level", _(""""""), HEADER_NAME, RESTORE), ("include_evidence", _("Add evidence header"), False, _("""You can have spambayes insert a header into mail, with the evidence that it used to classify that message (a collection of words with ham and spam probabilities). If you can view headers with your mailer, then this may give you some insight as to why a particular message was scored in a particular way."""), BOOLEAN, RESTORE), ("evidence_header_name", _("Evidence header name"), "X-Spambayes-Evidence", _(""""""), HEADER_NAME, RESTORE), ("mailid_header_name", _("Spambayes id header name"), "X-Spambayes-MailId", _(""""""), HEADER_NAME, RESTORE), ("include_trained", _("Add trained header"), True, _("""sb_mboxtrain.py and sb_filter.py can add a header that details how a message was trained, which lets you keep track of it, and appropriately re-train messages. However, if you would rather mboxtrain/sb_filter didn't rewrite the message files, you can disable this option."""), BOOLEAN, RESTORE), ("trained_header_name", _("Trained header name"), "X-Spambayes-Trained", _("""When training on a message, the name of the header to add with how it was trained"""), HEADER_NAME, RESTORE), ("clue_mailheader_cutoff", _("Debug header cutoff"), 0.5, _("""The range of clues that are added to the "debug" header in the E-mail. All clues that have their probability smaller than this number, or larger than one minus this number are added to the header such that you can see why spambayes thinks this is ham/spam or why it is unsure. The default is to show all clues, but you can reduce that by setting showclue to a lower value, such as 0.1"""), REAL, RESTORE), ("add_unique_id", _("Add unique spambayes id"), True, _("""If you wish to be able to find a specific message (via the 'find' box on the home page), or use the SMTP proxy to train using cached messages, you will need to know the unique id of each message. This option adds this information to a header added to each message."""), BOOLEAN, RESTORE), ("notate_to", _("Notate to"), (), _("""Some email clients (Outlook Express, for example) can only set up filtering rules on a limited set of headers. These clients cannot test for the existence/value of an arbitrary header and filter mail based on that information. To accommodate these kind of mail clients, you can add "spam", "ham", or "unsure" to the recipient list. A filter rule can then use this to see if one of these words (followed by a comma) is in the recipient list, and route the mail to an appropriate folder, or take whatever other action is supported and appropriate for the mail classification. As it interferes with replying, you may only wish to do this for spam messages; simply tick the boxes of the classifications take should be identified in this fashion."""), ((), _("ham"), _("spam"), _("unsure")), RESTORE), ("notate_subject", _("Classify in subject: header"), (), _("""This option will add the same information as 'Notate To', but to the start of the mail subject line."""), ((), _("ham"), _("spam"), _("unsure")), RESTORE), ), # pop3proxy settings: The only mandatory option is pop3proxy_servers, eg. # "pop3.my-isp.com:110", or a comma-separated list of those. The ":110" # is optional. If you specify more than one server in pop3proxy_servers, # you must specify the same number of ports in pop3proxy_ports. "pop3proxy" : ( ("remote_servers", _("Remote Servers"), (), _("""\ The SpamBayes POP3 proxy intercepts incoming email and classifies it before sending it on to your email client. You need to specify which POP3 server(s) and port(s) you wish it to connect to - a POP3 server address typically looks like 'pop3.myisp.net:110' where 'pop3.myisp.net' is the name of the computer where the POP3 server runs and '110' is the port on which the POP3 server listens. The other port you might find is '995', which is used for secure POP3. If you use more than one server, simply separate their names with commas. For example: 'pop3.myisp.net:110,pop.gmail.com:995'. You can get these server names and port numbers from your existing email configuration, or from your ISP or system administrator. If you are using Web-based email, you can't use the SpamBayes POP3 proxy (sorry!). In your email client's configuration, where you would normally put your POP3 server address, you should now put the address of the machine running SpamBayes. """), SERVER, DO_NOT_RESTORE), ("listen_ports", _("SpamBayes Ports"), (), _("""\ Each monitored POP3 server must be assigned to a different port in the SpamBayes POP3 proxy. You need to configure your email client to connect to this port instead of the actual remote POP3 server. If you don't know what port to use, try 8110 and go up from there. If you have two servers, your list of listen ports might then be '8110,8111'. """), SERVER, DO_NOT_RESTORE), ("allow_remote_connections", _("Allowed remote POP3 connections"), "localhost", _("""Enter a list of trusted IPs, separated by commas. Remote POP connections from any of them will be allowed. You can trust any IP using a single '*' as field value. You can also trust ranges of IPs using the '*' character as a wildcard (for instance 192.168.0.*). The localhost IP will always be trusted. Type 'localhost' in the field to trust this only address."""), IP_LIST, RESTORE), ("retrieval_timeout", _("Retrieval timeout"), 30, _("""When proxying messages, time out after this length of time if all the headers have been received. The rest of the mesasge will proxy straight through. Some clients have a short timeout period, and will give up on waiting for the message if this is too long. Note that the shorter this is, the less of long messages will be used for classifications (i.e. results may be effected)."""), REAL, RESTORE), ("use_ssl", "Connect via a secure socket layer", False, """Use SSL to connect to the server. This allows spambayes to connect without sending data in plain text. Note that this does not check the server certificate at this point in time.""", (False, True, "automatic"), DO_NOT_RESTORE), ), "smtpproxy" : ( ("remote_servers", _("Remote Servers"), (), _("""Use of the SMTP proxy is optional - if you would rather just train via the web interface, or the pop3dnd or mboxtrain scripts, then you can safely leave this option blank. The Spambayes SMTP proxy intercepts outgoing email - if you forward mail to one of the addresses below, it is examined for an id and the message corresponding to that id is trained as ham/spam. All other mail is sent along to your outgoing mail server. You need to specify which SMTP server(s) you wish it to intercept - a SMTP server address typically looks like "smtp.myisp.net". If you use more than one server, simply separate their names with commas. You can get these server names from your existing email configuration, or from your ISP or system administrator. If you are using Web-based email, you can't use the Spambayes SMTP proxy (sorry!). In your email client's configuration, where you would normally put your SMTP server address, you should now put the address of the machine running SpamBayes."""), SERVER, DO_NOT_RESTORE), ("listen_ports", _("SpamBayes Ports"), (), _("""Each SMTP server that is being monitored must be assigned to a 'port' in the Spambayes SMTP proxy. This port must be different for each monitored server, and there must be a port for each monitored server. Again, you need to configure your email client to use this port. If there are multiple servers, you must specify the same number of ports as servers, separated by commas."""), SERVER, DO_NOT_RESTORE), ("allow_remote_connections", _("Allowed remote SMTP connections"), "localhost", _("""Enter a list of trusted IPs, separated by commas. Remote SMTP connections from any of them will be allowed. You can trust any IP using a single '*' as field value. You can also trust ranges of IPs using the '*' character as a wildcard (for instance 192.168.0.*). The localhost IP will always be trusted. Type 'localhost' in the field to trust this only address. Note that you can unwittingly turn a SMTP server into an open proxy if you open this up, as connections to the server will appear to be from your machine, even if they are from a remote machine *through* your machine, to the server. We do not recommend opening this up fully (i.e. using '*'). """), IP_LIST, RESTORE), ("ham_address", _("Train as ham address"), "spambayes_ham@localhost", _("""When a message is received that you wish to train on (for example, one that was incorrectly classified), you need to forward or bounce it to one of two special addresses so that the SMTP proxy can identify it. If you wish to train it as ham, forward or bounce it to this address. You will want to use an address that is not a valid email address, like ham@nowhere.nothing."""), EMAIL_ADDRESS, RESTORE), ("spam_address", _("Train as spam address"), "spambayes_spam@localhost", _("""As with Ham Address above, but the address that you need to forward or bounce mail that you wish to train as spam. You will want to use an address that is not a valid email address, like spam@nowhere.nothing."""), EMAIL_ADDRESS, RESTORE), ("use_cached_message", _("Lookup message in cache"), False, _("""If this option is set, then the smtpproxy will attempt to look up the messages sent to it (for training) in the POP3 proxy cache or IMAP filter folders, and use that message as the training data. This avoids any problems where your mail client might change the message when forwarding, contaminating your training data. If you can be sure that this won't occur, then the id-lookup can be avoided. Note that Outlook Express users cannot use the lookup option (because of the way messages are forwarded), and so if they wish to use the SMTP proxy they must enable this option (but as messages are altered, may not get the best results, and this is not recommended)."""), BOOLEAN, RESTORE), ), # imap4proxy settings: The only mandatory option is imap4proxy_servers, eg. # "imap4.my-isp.com:143", or a comma-separated list of those. The ":143" # is optional. If you specify more than one server in imap4proxy_servers, # you must specify the same number of ports in imap4proxy_ports. "imap4proxy" : ( ("remote_servers", _("Remote Servers"), (), _("""The SpamBayes IMAP4 proxy intercepts incoming email and classifies it before sending it on to your email client. You need to specify which IMAP4 server(s) you wish it to intercept - a IMAP4 server address typically looks like "mail.myisp.net". If you use more than one server, simply separate their names with commas. You can get these server names from your existing email configuration, or from your ISP or system administrator. If you are using Web-based email, you can't use the SpamBayes IMAP4 proxy (sorry!). In your email client's configuration, where you would normally put your IMAP4 server address, you should now put the address of the machine running SpamBayes."""), SERVER, DO_NOT_RESTORE), ("listen_ports", _("SpamBayes Ports"), (), _("""Each IMAP4 server that is being monitored must be assigned to a 'port' in the SpamBayes IMAP4 proxy. This port must be different for each monitored server, and there must be a port for each monitored server. Again, you need to configure your email client to use this port. If there are multiple servers, you must specify the same number of ports as servers, separated by commas. If you don't know what to use here, and you only have one server, try 143, or if that doesn't work, try 8143."""), SERVER, DO_NOT_RESTORE), ("allow_remote_connections", _("Allowed remote IMAP4 connections"), "localhost", _("""Enter a list of trusted IPs, separated by commas. Remote IMAP connections from any of them will be allowed. You can trust any IP using a single '*' as field value. You can also trust ranges of IPs using the '*' character as a wildcard (for instance 192.168.0.*). The localhost IP will always be trusted. Type 'localhost' in the field to trust this only address."""), IP_LIST, RESTORE), ("use_ssl", "Connect via a secure socket layer", False, """Use SSL to connect to the server. This allows spambayes to connect without sending data in plain text. Note that this does not check the server certificate at this point in time.""", (False, True, "automatic"), DO_NOT_RESTORE), ), "html_ui" : ( ("port", _("Port"), 8880, _(""""""), PORT, RESTORE), ("launch_browser", _("Launch browser"), False, _("""If this option is set, then whenever sb_server or sb_imapfilter is started the default web browser will be opened to the main web interface page. Use of the -b switch when starting from the command line overrides this option."""), BOOLEAN, RESTORE), ("allow_remote_connections", _("Allowed remote UI connections"), "localhost", _("""Enter a list of trusted IPs, separated by commas. Remote connections from any of them will be allowed. You can trust any IP using a single '*' as field value. You can also trust ranges of IPs using the '*' character as a wildcard (for instance 192.168.0.*). The localhost IP will always be trusted. Type 'localhost' in the field to trust this only address."""), IP_LIST, RESTORE), ("display_headers", _("Headers to display in message review"), ("Subject", "From"), _("""When reviewing messages via the web user interface, you are presented with various information about the message. By default, you are shown the subject and who the message is from. You can add other message headers to display, however, such as the address the message is to, or the date that the message was sent."""), HEADER_NAME, RESTORE), ("display_received_time", _("Display date received in message review"), False, _("""When reviewing messages via the web user interface, you are presented with various information about the message. If you set this option, you will be shown the date that the message was received. """), BOOLEAN, RESTORE), ("display_score", _("Display score in message review"), False, _("""When reviewing messages via the web user interface, you are presented with various information about the message. If you set this option, this information will include the score that the message received when it was classified. You might wish to see this purely out of curiousity, or you might wish to only train on messages that score towards the boundaries of the classification areas. Note that in order to use this option, you must also enable the option to include the score in the message headers."""), BOOLEAN, RESTORE), ("display_adv_find", _("Display the advanced find query"), False, _("""Present advanced options in the 'Word Query' box on the front page, including wildcard and regular expression searching."""), BOOLEAN, RESTORE), ("default_ham_action", _("Default training for ham"), _("discard"), _("""When presented with the review list in the web interface, which button would you like checked by default when the message is classified as ham?"""), (_("ham"), _("spam"), _("discard"), _("defer")), RESTORE), ("default_spam_action", _("Default training for spam"), _("discard"), _("""When presented with the review list in the web interface, which button would you like checked by default when the message is classified as spam?"""), (_("ham"), _("spam"), _("discard"), _("defer")), RESTORE), ("default_unsure_action", _("Default training for unsure"), _("defer"), _("""When presented with the review list in the web interface, which button would you like checked by default when the message is classified as unsure?"""), (_("ham"), _("spam"), _("discard"), _("defer")), RESTORE), ("ham_discard_level", _("Ham Discard Level"), 0.0, _("""Hams scoring less than this percentage will default to being discarded in the training interface (they won't be trained). You'll need to turn off the 'Train when filtering' option, above, for this to have any effect"""), REAL, RESTORE), ("spam_discard_level", _("Spam Discard Level"), 100.0, _("""Spams scoring more than this percentage will default to being discarded in the training interface (they won't be trained). You'll need to turn off the 'Train when filtering' option, above, for this to have any effect"""), REAL, RESTORE), ("http_authentication", _("HTTP Authentication"), "None", _("""This option lets you choose the security level of the web interface. When selecting Basic or Digest, the user will be prompted a login and a password to access the web interface. The Basic option is faster, but transmits the password in clear on the network. The Digest option encrypts the password before transmission."""), ("None", "Basic", "Digest"), RESTORE), ("http_user_name", _("User name"), "admin", _("""If you activated the HTTP authentication option, you can modify the authorized user name here."""), r"[\w]+", RESTORE), ("http_password", _("Password"), "admin", _("""If you activated the HTTP authentication option, you can modify the authorized user password here."""), r"[\w]+", RESTORE), ("rows_per_section", _("Rows per section"), 10000, _("""Number of rows to display per ham/spam/unsure section."""), INTEGER, RESTORE), ), "imap" : ( ("server", _("Server"), (), _("""These are the names and ports of the imap servers that store your mail, and which the imap filter will connect to - for example: mail.example.com or imap.example.com:143. The default IMAP port is 143 (or 993 if using SSL); if you connect via one of those ports, you can leave this blank. If you use more than one server, use a comma delimited list of the server:port values."""), SERVER, DO_NOT_RESTORE), ("username", _("Username"), (), _("""This is the id that you use to log into your imap server. If your address is funkyguy@example.com, then your username is probably funkyguy."""), IMAP_ASTRING, DO_NOT_RESTORE), ("password", _("Password"), (), _("""That is that password that you use to log into your imap server. This will be stored in plain text in your configuration file, and if you have set the web user interface to allow remote connections, then it will be available for the whole world to see in plain text. If I've just freaked you out, don't panic . You can leave this blank and use the -p command line option to imapfilter.py and you will be prompted for your password."""), IMAP_ASTRING, DO_NOT_RESTORE), ("expunge", _("Purge//Expunge"), False, _("""Permanently remove *all* messages flagged with //Deleted on logout. If you do not know what this means, then please leave this as False."""), BOOLEAN, RESTORE), ("use_ssl", _("Connect via a secure socket layer"), False, _("""Use SSL to connect to the server. This allows spambayes to connect without sending the password in plain text. Note that this does not check the server certificate at this point in time."""), BOOLEAN, DO_NOT_RESTORE), ("filter_folders", _("Folders to filter"), ("INBOX",), _("""Comma delimited list of folders to be filtered"""), IMAP_FOLDER, DO_NOT_RESTORE), ("unsure_folder", _("Folder for unsure messages"), "", _(""""""), IMAP_FOLDER, DO_NOT_RESTORE), ("spam_folder", _("Folder for suspected spam"), "", _(""""""), IMAP_FOLDER, DO_NOT_RESTORE), ("ham_folder", _("Folder for ham messages"), "", _("""If you leave this option blank, messages classified as ham will not be moved. However, if you wish to have ham messages moved, you can select a folder here."""), IMAP_FOLDER, DO_NOT_RESTORE), ("ham_train_folders", _("Folders with mail to be trained as ham"), (), _("""Comma delimited list of folders that will be examined for messages to train as ham."""), IMAP_FOLDER, DO_NOT_RESTORE), ("spam_train_folders", _("Folders with mail to be trained as spam"), (), _("""Comma delimited list of folders that will be examined for messages to train as spam."""), IMAP_FOLDER, DO_NOT_RESTORE), ("move_trained_spam_to_folder", _("Folder to move trained spam to"), "", _("""When training, all messages in the spam training folder(s) (above) are examined - if they are new, they are used to train, if not, they are ignored. This examination does take time, however, so if speed is an issue for you, you may wish to move messages out of this folder once they have been trained (either to delete them or to a storage folder). If a folder name is specified here, this will happen automatically. Note that the filter is not yet clever enough to move the mail to different folders depending on which folder it was originally in - *all* messages will be moved to the same folder."""), IMAP_FOLDER, DO_NOT_RESTORE), ("move_trained_ham_to_folder", _("Folder to move trained ham to"), "", _("""When training, all messages in the ham training folder(s) (above) are examined - if they are new, they are used to train, if not, they are ignored. This examination does take time, however, so if speed is an issue for you, you may wish to move messages out of this folder once they have been trained (either to delete them or to a storage folder). If a folder name is specified here, this will happen automatically. Note that the filter is not yet clever enough to move the mail to different folders depending on which folder it was originally in - *all* messages will be moved to the same folder."""), IMAP_FOLDER, DO_NOT_RESTORE), ), "ZODB" : ( ("zeo_addr", _(""), "", _(""""""), IMAP_ASTRING, DO_NOT_RESTORE), ("event_log_file", _(""), "", _(""""""), IMAP_ASTRING, RESTORE), ("folder_dir", _(""), "", _(""""""), PATH, DO_NOT_RESTORE), ("ham_folders", _(""), "", _(""""""), PATH, DO_NOT_RESTORE), ("spam_folders", _(""), "", _(""""""), PATH, DO_NOT_RESTORE), ("event_log_severity", _(""), 0, _(""""""), INTEGER, RESTORE), ("cache_size", _(""), 2000, _(""""""), INTEGER, RESTORE), ), "imapserver" : ( ("username", _("Username"), "", _("""The username to use when logging into the SpamBayes IMAP server."""), IMAP_ASTRING, DO_NOT_RESTORE), ("password", _("Password"), "", _("""The password to use when logging into the SpamBayes IMAP server."""), IMAP_ASTRING, DO_NOT_RESTORE), ("port", _("IMAP Listen Port"), 143, _("""The port to serve the SpamBayes IMAP server on."""), PORT, RESTORE), ), "globals" : ( ("verbose", _("Verbose"), False, _(""""""), BOOLEAN, RESTORE), ("dbm_type", _("Database storage type"), "best", _("""What DBM storage type should we use? Must be best, db3hash, dbhash or gdbm. Windows folk should steer clear of dbhash. Default is "best", which will pick the best DBM type available on your platform."""), ("best", "db3hash", "dbhash", "gdbm"), RESTORE), ("proxy_username", _("HTTP Proxy Username"), "", _("""The username to give to the HTTP proxy when required. If a username is not necessary, simply leave blank."""), r"[\w]+", DO_NOT_RESTORE), ("proxy_password", _("HTTP Proxy Password"), "", _("""The password to give to the HTTP proxy when required. This is stored in clear text in your configuration file, so if that bothers you then don't do this. You'll need to use a proxy that doesn't need authentication, or do without any SpamBayes HTTP activity."""), r"[\w]+", DO_NOT_RESTORE), ("proxy_server", _("HTTP Proxy Server"), "", _("""If a spambayes application needs to use HTTP, it will try to do so through this proxy server. The port defaults to 8080, or can be entered with the server:port form."""), SERVER, DO_NOT_RESTORE), ("language", _("User Interface Language"), ("en_US",), _("""If possible, the user interface should use a language from this list (in order of preference)."""), r"\w\w(?:_\w\w)?", RESTORE), ), "Plugin": ( ("xmlrpc_path", _("XML-RPC path"), "/sbrpc", _("""The path to respond to."""), r"[\w]+", RESTORE), ("xmlrpc_host", _("XML-RPC host"), "localhost", _("""The host to listen on."""), SERVER, RESTORE), ("xmlrpc_port", _("XML-RPC port"), 8001, _("""The port to listen on."""), r"[\d]+", RESTORE), ), } # `optionsPathname` is the pathname of the last ini file in the list. # This is where the web-based configuration page will write its changes. # If no ini files are found, it defaults to bayescustomize.ini in the # current working directory. optionsPathname = None # The global options object - created by load_options options = None def load_options(): global optionsPathname, options options = OptionsClass() options.load_defaults(defaults) # Maybe we are reloading. if optionsPathname: options.merge_file(optionsPathname) alternate = None if hasattr(os, 'getenv'): alternate = os.getenv('BAYESCUSTOMIZE') if alternate: filenames = alternate.split(os.pathsep) options.merge_files(filenames) optionsPathname = os.path.abspath(filenames[-1]) else: alts = [] for path in ['bayescustomize.ini', '~/.spambayesrc']: epath = os.path.expanduser(path) if os.path.exists(epath): alts.append(epath) if alts: options.merge_files(alts) optionsPathname = os.path.abspath(alts[-1]) if not optionsPathname: optionsPathname = os.path.abspath('bayescustomize.ini') if sys.platform.startswith("win") and \ not os.path.isfile(optionsPathname): # If we are on Windows and still don't have an INI, default to the # 'per-user' directory. try: from win32com.shell import shell, shellcon except ImportError: # We are on Windows, with no BAYESCUSTOMIZE set, no ini file # in the current directory, and no win32 extensions installed # to locate the "user" directory - seeing things are so lamely # setup, it is worth printing a warning print >> sys.stderr, "NOTE: We can not locate an INI file " \ "for SpamBayes, and the Python for Windows extensions " \ "are not installed, meaning we can't locate your " \ "'user' directory. An empty configuration file at " \ "'%s' will be used." % optionsPathname.encode('mbcs') else: windowsUserDirectory = os.path.join( shell.SHGetFolderPath(0,shellcon.CSIDL_APPDATA,0,0), "SpamBayes", "Proxy") try: if not os.path.isdir(windowsUserDirectory): os.makedirs(windowsUserDirectory) except os.error: # unable to make the directory - stick to default. pass else: optionsPathname = os.path.join(windowsUserDirectory, 'bayescustomize.ini') # Not everyone is unicode aware - keep it a string. optionsPathname = optionsPathname.encode("mbcs") # If the file exists, then load it. if os.path.exists(optionsPathname): options.merge_file(optionsPathname) def get_pathname_option(section, option): """Return the option relative to the path specified in the gloabl optionsPathname, unless it is already an absolute path.""" filename = os.path.expanduser(options.get(section, option)) if os.path.isabs(filename): return filename return os.path.join(os.path.dirname(optionsPathname), filename) # Ideally, we should not create the objects at import time - but we have # done it this way forever! # We avoid having the options loading code at the module level, as then # the only way to re-read is to reload this module, and as at 2.3, that # doesn't work in a .zip file. load_options() spambayes-1.1a6/spambayes/OptionsClass.py0000664000076500000240000010250211116632272020641 0ustar skipstaff00000000000000"""OptionsClass Classes: Option - Holds information about an option OptionsClass - A collection of options Abstract: This module is used to manage "options" managed in user editable files. This is the implementation of the Options.options globally shared options object for the SpamBayes project, but is also able to be used to manage other options required by each application. The Option class holds information about an option - the name of the option, a nice name (to display), documentation, default value, possible values (a tuple or a regex pattern), whether multiple values are allowed, and whether the option should be reset when restoring to defaults (options like server names should *not* be). The OptionsClass class provides facility for a collection of Options. It is expected that manipulation of the options will be carried out via an instance of this class. Experimental or deprecated options are prefixed with 'x-', borrowing the practice from RFC-822 mail. If the user sets an option like: [Tokenizer] x-transmogrify: True and an 'x-transmogrify' or 'transmogrify' option exists, it is set silently to the value given by the user. If the user sets an option like: [Tokenizer] transmogrify: True and no 'transmogrify' option exists, but an 'x-transmogrify' option does, the latter is set to the value given by the users and a deprecation message is printed to standard error. To Do: o Stop allowing invalid options in configuration files o Find a regex expert to come up with *good* patterns for domains, email addresses, and so forth. o str(Option) should really call Option.unconvert since this is what it does. Try putting that in and running all the tests. o [See also the __issues__ string.] o Suggestions? """ # This module is part of the spambayes project, which is Copyright 2002-2007 # The Python Software Foundation and is covered by the Python Software # Foundation license. __credits__ = "All the Spambayes folk." # blame for the new format: Tony Meyer __issues__ = """Things that should be considered further and by other people: We are very generous in checking validity when multiple values are allowed and the check is a regex (rather than a tuple). Any sequence that does not match the regex may be used to delimit the values. For example, if the regex was simply r"[\d]*" then these would all be considered valid: "123a234" -> 123, 234 "123abced234" -> 123, 234 "123XST234xas" -> 123, 234 "123 234" -> 123, 234 "123~!@$%^&@234!" -> 123, 234 If this is a problem, my recommendation would be to change the multiple_values_allowed attribute from a boolean to a regex/None i.e. if multiple is None, then only one value is allowed. Otherwise multiple is used in a re.split() to separate the input. """ import sys import os import shutil from tempfile import TemporaryFile try: import cStringIO as StringIO except ImportError: import StringIO import re import types import locale from textwrap import wrap __all__ = ['OptionsClass', 'HEADER_NAME', 'HEADER_VALUE', 'INTEGER', 'REAL', 'BOOLEAN', 'SERVER', 'PORT', 'EMAIL_ADDRESS', 'PATH', 'VARIABLE_PATH', 'FILE', 'FILE_WITH_PATH', 'IMAP_FOLDER', 'IMAP_ASTRING', 'RESTORE', 'DO_NOT_RESTORE', 'IP_LIST', 'OCRAD_CHARSET', ] MultiContainerTypes = (types.TupleType, types.ListType) class Option(object): def __init__(self, name, nice_name="", default=None, help_text="", allowed=None, restore=True): self.name = name self.nice_name = nice_name self.default_value = default self.explanation_text = help_text self.allowed_values = allowed self.restore = restore self.delimiter = None # start with default value self.set(default) def display_name(self): '''A name for the option suitable for display to a user.''' return self.nice_name def default(self): '''The default value for the option.''' return self.default_value def doc(self): '''Documentation for the option.''' return self.explanation_text def valid_input(self): '''Valid values for the option.''' return self.allowed_values def no_restore(self): '''Do not restore this option when restoring to defaults.''' return not self.restore def set(self, val): '''Set option to value.''' self.value = val def get(self): '''Get option value.''' return self.value def multiple_values_allowed(self): '''Multiple values are allowed for this option.''' return type(self.default_value) in MultiContainerTypes def is_valid(self, value): '''Check if this is a valid value for this option.''' if self.allowed_values is None: return False if self.multiple_values_allowed(): return self.is_valid_multiple(value) else: return self.is_valid_single(value) def is_valid_multiple(self, value): '''Return True iff value is a valid value for this option. Use if multiple values are allowed.''' if type(value) in MultiContainerTypes: for val in value: if not self.is_valid_single(val): return False return True return self.is_valid_single(value) def is_valid_single(self, value): '''Return True iff value is a valid value for this option. Use when multiple values are not allowed.''' if type(self.allowed_values) == types.TupleType: if value in self.allowed_values: return True else: return False else: # special handling for booleans, thanks to Python 2.2 if self.is_boolean and (value == True or value == False): return True if type(value) != type(self.value) and \ type(self.value) not in MultiContainerTypes: # This is very strict! If the value is meant to be # a real number and an integer is passed in, it will fail. # (So pass 1. instead of 1, for example) return False if value == "": # A blank string is always ok. return True avals = self._split_values(value) # in this case, allowed_values must be a regex, and # _split_values must match once and only once if len(avals) == 1: return True else: # either no match or too many matches return False def _split_values(self, value): # do the regex mojo here if not self.allowed_values: return ('',) try: r = re.compile(self.allowed_values) except: print >> sys.stderr, self.allowed_values raise s = str(value) i = 0 vals = [] while True: m = r.search(s[i:]) if m is None: break vals.append(m.group()) delimiter = s[i:i + m.start()] if self.delimiter is None and delimiter != "": self.delimiter = delimiter i += m.end() return tuple(vals) def as_nice_string(self, section=None): '''Summarise the option in a user-readable format.''' if section is None: strval = "" else: strval = "[%s] " % (section) strval += "%s - \"%s\"\nDefault: %s\nDo not restore: %s\n" \ % (self.name, self.display_name(), str(self.default()), str(self.no_restore())) strval += "Valid values: %s\nMultiple values allowed: %s\n" \ % (str(self.valid_input()), str(self.multiple_values_allowed())) strval += "\"%s\"\n\n" % (str(self.doc())) return strval def as_documentation_string(self, section=None): '''Summarise the option in a format suitable for unmodified insertion in HTML documentation.''' strval = [""] if section is not None: strval.append("\t[%s]" % (section,)) strval.append("\t%s" % (self.name,)) strval.append("\t%s" % \ ", ".join([str(s) for s in self.valid_input()])) default = self.default() if isinstance(default, types.TupleType): default = ", ".join([str(s) for s in default]) else: default = str(default) strval.append("\t%s" % (default,)) strval.append("\t%s: %s" \ % (self.display_name(), self.doc())) strval.append("\n") return "\n".join(strval) def write_config(self, file): '''Output value in configuration file format.''' file.write(self.name) file.write(': ') file.write(self.unconvert()) file.write('\n') def convert(self, value): '''Convert value from a string to the appropriate type.''' svt = type(self.value) if svt == type(value): # already the correct type return value if type(self.allowed_values) == types.TupleType and \ value in self.allowed_values: # already correct type return value if self.is_boolean(): if str(value) == "True" or value == 1: return True elif str(value) == "False" or value == 0: return False raise TypeError, self.name + " must be True or False" if self.multiple_values_allowed(): # This will fall apart if the allowed_value is a tuple, # but not a homogenous one... if isinstance(self.allowed_values, types.StringTypes): vals = list(self._split_values(value)) else: if isinstance(value, types.TupleType): vals = list(value) else: vals = value.split() if len(self.default_value) > 0: to_type = type(self.default_value[0]) else: to_type = types.StringType for i in range(0, len(vals)): vals[i] = self._convert(vals[i], to_type) return tuple(vals) else: return self._convert(value, svt) raise TypeError, self.name + " has an invalid type." def _convert(self, value, to_type): '''Convert an int, float or string to the specified type.''' if to_type == type(value): # already the correct type return value if to_type == types.IntType: return locale.atoi(value) if to_type == types.FloatType: return locale.atof(value) if to_type in types.StringTypes: return str(value) raise TypeError, "Invalid type." def unconvert(self): '''Convert value from the appropriate type to a string.''' if type(self.value) in types.StringTypes: # nothing to do return self.value if self.is_boolean(): # A wee bit extra for Python 2.2 if self.value == True: return "True" else: return "False" if type(self.value) == types.TupleType: if len(self.value) == 0: return "" if len(self.value) == 1: v = self.value[0] if type(v) == types.FloatType: return locale.str(self.value[0]) return str(v) # We need to separate out the items strval = "" # We use a character that is invalid as the separator # so that it will reparse correctly. We could try all # characters, but we make do with this set of commonly # used ones - note that the first one that works will # be used. Perhaps a nicer solution than this would be # to specifiy a valid delimiter for all options that # can have multiple values. Note that we have None at # the end so that this will crash and die if none of # the separators works . if self.delimiter is None: if type(self.allowed_values) == types.TupleType: self.delimiter = ' ' else: v0 = self.value[0] v1 = self.value[1] for sep in [' ', ',', ':', ';', '/', '\\', None]: # we know at this point that len(self.value) is at # least two, because len==0 and len==1 were dealt # with as special cases test_str = str(v0) + sep + str(v1) test_tuple = self._split_values(test_str) if test_tuple[0] == str(v0) and \ test_tuple[1] == str(v1) and \ len(test_tuple) == 2: break # cache this so we don't always need to do the above self.delimiter = sep for v in self.value: if type(v) == types.FloatType: v = locale.str(v) else: v = str(v) strval += v + self.delimiter strval = strval[:-len(self.delimiter)] # trailing seperator else: # Otherwise, we just hope str() will do the job strval = str(self.value) return strval def is_boolean(self): '''Return True iff the option is a boolean value.''' # This is necessary because of the Python 2.2 True=1, False=0 # cheat. The valid values are returned as 0 and 1, even if # they are actually False and True - but 0 and 1 are not # considered valid input (and 0 and 1 don't look as nice) # So, just for the 2.2 people, we have this helper function try: if type(self.allowed_values) == types.TupleType and \ len(self.allowed_values) > 0 and \ type(self.allowed_values[0]) == types.BooleanType: return True return False except AttributeError: # If the user has Python 2.2 and an option has valid values # of (0, 1) - i.e. integers, then this function will return # the wrong value. I don't know what to do about that without # explicitly stating which options are boolean if self.allowed_values == (False, True): return True return False class OptionsClass(object): def __init__(self): self.verbose = None self._options = {} self.restore_point = {} self.conversion_table = {} # set by creator if they need it. # # Regular expressions for parsing section headers and options. # Lifted straight from ConfigParser # SECTCRE = re.compile( r'\[' # [ r'(?P

    [^]]+)' # very permissive! r'\]' # ] ) OPTCRE = re.compile( r'(?P