from __future__ import generators
import cPickle
import os
import sys
import errno
import shutil
import traceback
import operator
import win32api, win32con, win32gui
import win32com.client
import win32com.client.gencache
import pythoncom
import msgstore
import oastats
try:
True, False
except NameError:
# Maintain compatibility with Python 2.2
True, False = 1, 0
# 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:
try:
this_filename = os.path.abspath(__file__)
except NameError: # no __file__ - means Py2.2 and __name__=='__main__'
this_filename = os.path.abspath(sys.argv[0])
# See if we can use the new bsddb module. (The old one is unreliable
# on Windows, so we don't use that)
try:
import bsddb3 as bsddb
# bsddb3 is definitely not broken
use_db = True
except ImportError:
# Not using the 3rd party bsddb3, so try the one in the std library
try:
import bsddb
use_db = hasattr(bsddb, "db") # This name is not in the old one.
except ImportError:
# No DB library at all!
assert not hasattr(sys, "frozen"), \
"Don't build binary versions without bsddb!"
use_db = False
# This is a little bit of a hack <wink>. 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():
try:
from spambayes import OptionsClass
except ImportError:
parent = os.path.abspath(os.path.join(os.path.dirname(this_filename),
".."))
sys.path.insert(0, parent)
def import_core_spambayes_stuff(ini_filenames):
global bayes_classifier, bayes_tokenize, bayes_storage
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
bayes_classifier = classifier
bayes_tokenize = tokenize
bayes_storage = storage
assert "spambayes.Options" in sys.modules, \
"Expected 'spambayes.Options' to be loaded here"
# 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.
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 + self.db_extension
self.mdb_filename = mdb_base_name + 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):
raise NotImplementedError
def close_bayes(self, bayes):
bayes.close()
class PickleStorageManager(BasicStorageManager):
db_extension = ".pck"
def open_bayes(self):
return bayes_storage.PickledClassifier(self.bayes_filename)
def open_mdb(self):
return cPickle.load(open(self.mdb_filename, 'rb'))
def new_mdb(self):
return {}
def store_mdb(self, mdb):
SavePickle(mdb, self.mdb_filename)
def close_mdb(self, mdb):
pass
def is_incremental(self):
return False # False means we always save the entire DB
class DBStorageManager(BasicStorageManager):
db_extension = ".db"
def open_bayes(self):
# bsddb doesn't handle unicode filenames yet :(
fname = self.bayes_filename.encode(filesystem_encoding)
return bayes_storage.DBDictClassifier(fname)
def open_mdb(self):
fname = self.mdb_filename.encode(filesystem_encoding)
return bsddb.hashopen(fname)
def new_mdb(self):
try:
os.unlink(self.mdb_filename)
except EnvironmentError, e:
if e.errno != errno.ENOENT: raise
return self.open_mdb()
def store_mdb(self, mdb):
mdb.sync()
def close_mdb(self, mdb):
mdb.close()
def is_incremental(self):
return True # True means only changed records get actually written
# 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))
if len(message_db) != bayes.nham + bayes.nspam:
print "*** - message database has %d messages - bayes has %d - something is screwey" % \
(len(message_db), bayes.nham + bayes.nspam)
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
# Try and work out where this count sometimes goes wrong.
if bayes.nspam + bayes.nham != len(self.message_db):
print "WARNING: Bayes database has %d messages, " \
"but training database has %d" % \
(bayes.nspam + bayes.nham, len(self.message_db))
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():
return [PickleStorageManager, DBStorageManager][use_db]
# Our main "bayes manager"
class BayesManager:
def __init__(self, config_base="default", outlook=None, verbose=0):
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
import_early_core_spambayes_stuff()
self.application_directory = os.path.dirname(this_filename)
# 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 type(value) == type(u''), "%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
# Now we have the data directory, migrate anything needed, and load
# any config from it.
self.MigrateDataDirectory()
# 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).
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)
import_core_spambayes_stuff(bayes_option_filenames)
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)
self.LoadBayes()
self.stats = oastats.Stats(self.config)
# "old" bayes functions - new code should use "classifier_data" directly
def LoadBayes(self):
try:
self.classifier_data.Load()
except:
self.ReportFatalStartupError("Failed to load bayes database")
self.classifier_data.InitNew()
def InitNewBayes(self):
self.classifier_data.InitNew()
def SaveBayes(self):
self.classifier_data.Save()
def SaveBayesPostIncrementalTrain(self):
self.classifier_data.SavePostIncrementalTrain()
# Logging - this too 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 MigrateDataDirectory(self):
# A bit of a nod to save people doing a full retrain.
# Try and locate our files in the old location, and move
# them to the new one.
# Note that this is migrating data for very old versions of the
# plugin (before the first decent binary!). The next time it is
# touched it can die :)
self._MigrateFile("default_bayes_database.pck")
self._MigrateFile("default_bayes_database.db")
self._MigrateFile("default_message_database.pck")
self._MigrateFile("default_message_database.db")
self._MigrateFile("default_configuration.pck")
# Copy a file from the application_directory to the data_directory.
# By default (do_move not specified), the source file is deleted.
# Pass do_move=False to leave the original file.
def _MigrateFile(self, filename):
src = os.path.join(self.application_directory, filename)
dest = os.path.join(self.data_directory, filename)
if os.path.isfile(src) and not os.path.isfile(dest):
# shutil in 2.2 and earlier don't contain 'move'.
# Win95 and Win98 don't support MoveFileEx.
shutil.copyfile(src, dest)
os.remove(src)
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 = "<unknown folder>"
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 expose 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 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.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"""
else:
# xxx - remove me sometime - win32all grew this post 154(ish)
# binary never released with this, so we can be a little more brutal
# Try and rename to current profile, silent failure
try:
os.rename(os.path.join(self.data_directory, "unknown_profile.ini"),
os.path.join(self.data_directory, profile_name + ".ini"))
except os.error:
pass
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.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 folders."
# 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 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)
_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))
syntax highlighted by Code2HTML, v. 0.9.1