[Spambayes-checkins] spambayes/Outlook2000 addin.py,1.24,1.25
Mark Hammond
mhammond@users.sourceforge.net
Mon Nov 4 00:52:12 2002
Update of /cvsroot/spambayes/spambayes/Outlook2000
In directory usw-pr-cvs1:/tmp/cvs-serv29880
Modified Files:
addin.py
Log Message:
New "Delete As Spam" button, complete with button image, and the
button changes appearance and behaviour when one of the spam
folders is selected.
Index: addin.py
===================================================================
RCS file: /cvsroot/spambayes/spambayes/Outlook2000/addin.py,v
retrieving revision 1.24
retrieving revision 1.25
diff -C2 -d -r1.24 -r1.25
*** addin.py 1 Nov 2002 23:54:03 -0000 1.24
--- addin.py 4 Nov 2002 00:52:10 -0000 1.25
***************
*** 1,5 ****
# SpamBayes Outlook Addin
! import sys
import warnings
--- 1,5 ----
# SpamBayes Outlook Addin
! import sys, os
import warnings
***************
*** 16,19 ****
--- 16,21 ----
import win32ui
+ import win32gui, win32con, win32clipboard # for button images!
+
# If we are not running in a console, redirect all print statements to the
# win32traceutil collector.
***************
*** 28,38 ****
! # A lovely big block that attempts to catch the most common errors - COM objects not installed.
try:
! # Support for COM objects we use.
gencache.EnsureModule('{00062FFF-0000-0000-C000-000000000046}', 0, 9, 0, bForDemand=True) # Outlook 9
gencache.EnsureModule('{2DF8D04C-5BFA-101B-BDE5-00AA0044DE52}', 0, 2, 1, bForDemand=True) # Office 9
! # The TLB defiining the interfaces we implement
universal.RegisterInterfaces('{AC0714F2-3D04-11D1-AE7D-00A0C90F26F4}', 0, 1, 0, ["_IDTExtensibility2"])
except pythoncom.com_error, (hr, msg, exc, arg):
--- 30,40 ----
! # Attempt to catch the most common errors - COM objects not installed.
try:
! # Generate support so we get complete support including events
gencache.EnsureModule('{00062FFF-0000-0000-C000-000000000046}', 0, 9, 0, bForDemand=True) # Outlook 9
gencache.EnsureModule('{2DF8D04C-5BFA-101B-BDE5-00AA0044DE52}', 0, 2, 1, bForDemand=True) # Office 9
! # Register what vtable based interfaces we need to implement.
universal.RegisterInterfaces('{AC0714F2-3D04-11D1-AE7D-00A0C90F26F4}', 0, 1, 0, ["_IDTExtensibility2"])
except pythoncom.com_error, (hr, msg, exc, arg):
***************
*** 46,76 ****
if exc:
print "Exception: %s" % (exc)
! print "Sorry, I can't be more help, but I can't continue while I have this error."
sys.exit(1)
! # Something that should be in win32com in some form or another.
def CastToClone(ob, target):
"""'Cast' a COM object to another type"""
- # todo - should support target being an IID
if hasattr(target, "index"): # string like
# for now, we assume makepy for this to work.
if not ob.__class__.__dict__.has_key("CLSID"):
- # Eeek - no makepy support - try and build it.
ob = gencache.EnsureDispatch(ob)
if not ob.__class__.__dict__.has_key("CLSID"):
raise ValueError, "Must be a makepy-able object for this to work"
clsid = ob.CLSID
- # Lots of hoops to support "demand-build" - ie, generating
- # code for an interface first time it is used. We assume the
- # interface name exists in the same library as the object.
- # This is generally the case - only referenced typelibs may be
- # a problem, and we can handle that later. Maybe <wink>
- # So get the generated module for the library itself, then
- # find the interface CLSID there.
mod = gencache.GetModuleForCLSID(clsid)
- # Get the 'root' module.
mod = gencache.GetModuleForTypelib(mod.CLSID, mod.LCID,
mod.MajorVersion, mod.MinorVersion)
- # Find the CLSID of the target
# XXX - should not be looking in VTables..., but no general map currently exists
# (Fixed in win32all!)
--- 48,69 ----
if exc:
print "Exception: %s" % (exc)
! print "Sorry I can't be more help, but I can't continue while I have this error."
sys.exit(1)
! # A couple of functions that are in new win32all, but we dont want to
! # force people to ugrade if we can avoid it.
! # NOTE: Most docstrings and comments removed - see the win32all version
def CastToClone(ob, target):
"""'Cast' a COM object to another type"""
if hasattr(target, "index"): # string like
# for now, we assume makepy for this to work.
if not ob.__class__.__dict__.has_key("CLSID"):
ob = gencache.EnsureDispatch(ob)
if not ob.__class__.__dict__.has_key("CLSID"):
raise ValueError, "Must be a makepy-able object for this to work"
clsid = ob.CLSID
mod = gencache.GetModuleForCLSID(clsid)
mod = gencache.GetModuleForTypelib(mod.CLSID, mod.LCID,
mod.MajorVersion, mod.MinorVersion)
# XXX - should not be looking in VTables..., but no general map currently exists
# (Fixed in win32all!)
***************
*** 81,85 ****
mod = gencache.GetModuleForCLSID(target_clsid)
target_class = getattr(mod, target)
- # resolve coclass to interface
target_class = getattr(target_class, "default_interface", target_class)
return target_class(ob) # auto QI magic happens
--- 74,77 ----
***************
*** 90,93 ****
--- 82,118 ----
CastTo = CastToClone
+ # Something else in later win32alls - like "DispatchWithEvents", but the
+ # returned object is not both the Dispatch *and* the event handler
+ def WithEventsClone(clsid, user_event_class):
+ clsid = getattr(clsid, "_oleobj_", clsid)
+ disp = Dispatch(clsid)
+ if not disp.__dict__.get("CLSID"): # Eeek - no makepy support - try and build it.
+ try:
+ ti = disp._oleobj_.GetTypeInfo()
+ disp_clsid = ti.GetTypeAttr()[0]
+ tlb, index = ti.GetContainingTypeLib()
+ tla = tlb.GetLibAttr()
+ mod = gencache.EnsureModule(tla[0], tla[1], tla[3], tla[4])
+ disp_class = gencache.GetClassForProgID(str(disp_clsid))
+ except pythoncom.com_error:
+ raise TypeError, "This COM object can not automate the makepy process - please run makepy manually for this object"
+ else:
+ disp_class = disp.__class__
+ clsid = disp_class.CLSID
+ import new
+ events_class = getevents(clsid)
+ if events_class is None:
+ raise ValueError, "This COM object does not support events."
+ result_class = new.classobj("COMEventClass", (events_class, user_event_class), {})
+ instance = result_class(disp) # This only calls the first base class __init__.
+ if hasattr(user_event_class, "__init__"):
+ user_event_class.__init__(instance)
+ return instance
+
+ try:
+ from win32com.client import WithEvents
+ except ImportError: # appears in 151 and later.
+ WithEvents = WithEventsClone
+
# Whew - we seem to have all the COM support we need - let's rock!
***************
*** 97,101 ****
self.handler = handler
self.args = args
!
def OnClick(self, button, cancel):
self.handler(*self.args)
--- 122,127 ----
self.handler = handler
self.args = args
! def Close(self):
! self.handler = self.args = None
def OnClick(self, button, cancel):
self.handler(*self.args)
***************
*** 107,110 ****
--- 133,138 ----
self.manager = manager
self.target = target
+ def Close(self):
+ self.application = self.manager = self.target = None
class FolderItemsEvent(_BaseItemsEvent):
***************
*** 172,195 ****
assert train.been_trained_as_spam(msgstore_message, self.manager)
def ShowClues(mgr, app):
from cgi import escape
! sel = app.ActiveExplorer().Selection
! if sel.Count == 0:
! win32ui.MessageBox("No items are selected", "No selection")
! return
! if sel.Count > 1:
! win32ui.MessageBox("Please select a single item", "Large selection")
! return
!
! item = sel.Item(1)
! if item.Class != constants.olMail:
! win32ui.MessageBox("This function can only be performed on mail items",
! "Not a mail message")
return
!
! msgstore_message = mgr.message_store.GetMessage(item)
score, clues = mgr.score(msgstore_message, evidence=True, scale=False)
new_msg = app.CreateItem(0)
body = ["<h2>Spam Score: %g</h2><br>" % score]
push = body.append
--- 200,217 ----
assert train.been_trained_as_spam(msgstore_message, self.manager)
+ # Event function fired from the "Show Clues" UI items.
def ShowClues(mgr, app):
from cgi import escape
! msgstore_message = mgr.addin.GetSelectedMessages(False)
! if msgstore_message is None:
return
! item = msgstore_message.GetOutlookItem()
score, clues = mgr.score(msgstore_message, evidence=True, scale=False)
new_msg = app.CreateItem(0)
+ # NOTE: Silly Outlook always switches the message editor back to RTF
+ # once the Body property has been set. Thus, there is no reasonable
+ # way to get this as text only. Next best then is to use HTML, 'cos at
+ # least we know how to exploit it!
body = ["<h2>Spam Score: %g</h2><br>" % score]
push = body.append
***************
*** 210,215 ****
new_msg.Subject = "Spam Clues: " + item.Subject
! # Stupid outlook always switches to RTF :( Work-around
! ## new_msg.Body = body
new_msg.HTMLBody = "<HTML><BODY>" + body + "</BODY></HTML>"
# Attach the source message to it
--- 232,236 ----
new_msg.Subject = "Spam Clues: " + item.Subject
! # As above, use HTMLBody else Outlook refuses to behave.
new_msg.HTMLBody = "<HTML><BODY>" + body + "</BODY></HTML>"
# Attach the source message to it
***************
*** 218,221 ****
--- 239,359 ----
new_msg.Display()
+ # The "Delete As Spam" and "Recover Spam" button
+ # The event from Outlook's explorer that our folder has changed.
+ class ButtonDeleteAsExplorerEvent:
+ def Init(self, but):
+ self.but = but
+ def Close(self):
+ self.but = None
+ def OnFolderSwitch(self):
+ self.but._UpdateForFolderChange()
+
+ class ButtonDeleteAsEvent:
+ def Init(self, manager, application, explorer):
+ # NOTE - keeping a reference to 'explorer' in this event
+ # appears to cause an Outlook circular reference, and outlook
+ # never terminates (it does close, but the process remains alive)
+ # This is why we needed to use WithEvents, so the event class
+ # itself doesnt keep such a reference (and we need to keep a ref
+ # to the event class so it doesn't auto-disconnect!)
+ self.manager = manager
+ self.application = application
+ self.explorer_events = WithEvents(explorer,
+ ButtonDeleteAsExplorerEvent)
+ self.set_for_as_spam = None
+ self.explorer_events.Init(self)
+ self._UpdateForFolderChange()
+
+ def Close(self):
+ self.manager = self.application = self.explorer = None
+
+ def _UpdateForFolderChange(self):
+ explorer = self.application.ActiveExplorer()
+ if explorer is None:
+ print "** Folder Change, but don't have an explorer"
+ return
+ outlook_folder = explorer.CurrentFolder
+ is_spam = False
+ if outlook_folder is not None:
+ mapi_folder = self.manager.message_store.GetFolder(outlook_folder)
+ look_id = self.manager.config.filter.spam_folder_id
+ if look_id:
+ look_folder = self.manager.message_store.GetFolder(look_id)
+ if mapi_folder == look_folder:
+ is_spam = True
+ if not is_spam:
+ look_id = self.manager.config.filter.unsure_folder_id
+ if look_id:
+ look_folder = self.manager.message_store.GetFolder(look_id)
+ if mapi_folder == look_folder:
+ is_spam = True
+ if is_spam:
+ set_for_as_spam = False
+ else:
+ set_for_as_spam = True
+ if set_for_as_spam != self.set_for_as_spam:
+ if set_for_as_spam:
+ image = "delete_as_spam.bmp"
+ self.Caption = "Delete As Spam"
+ self.TooltipText = \
+ "Move the selected message to the Spam folder,\n" \
+ "and train the system that this is Spam."
+ else:
+ image = "recover_ham.bmp"
+ self.Caption = "Recover from Spam"
+ self.TooltipText = \
+ "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"
+ # Set the image.
+ print "Setting image to", image
+ SetButtonImage(self, image)
+ self.set_for_as_spam = set_for_as_spam
+
+ def OnClick(self, button, cancel):
+ msgstore = self.manager.message_store
+ msgstore_messages = self.manager.addin.GetSelectedMessages(True)
+ if not msgstore_messages:
+ return
+ if self.set_for_as_spam:
+ # Delete this item as spam.
+ spam_folder_id = self.manager.config.filter.spam_folder_id
+ spam_folder = msgstore.GetFolder(spam_folder_id)
+ if not spam_folder:
+ win32ui.MessageBox("You must configure the Spam folder",
+ "Invalid Configuration")
+ return
+ import train
+ for msgstore_message in msgstore_messages:
+ # Must train before moving, else we lose the message!
+ print "Training on message - ",
+ if train.train_message(msgstore_message, True, self.manager):
+ print "trained as spam"
+ else:
+ print "already was trained as spam"
+ # Now move it.
+ msgstore_message.MoveTo(spam_folder)
+ else:
+ win32ui.MessageBox("Please be patient <wink>")
+
+ # Helpers to work with images on buttons/toolbars.
+ def SetButtonImage(button, fname):
+ # 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 -
+ # this, we can not simply perform this load once and reuse the image.
+ if not os.path.isabs(fname):
+ fname = os.path.join( os.path.dirname(__file__), "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()
+
# The outlook Plugin COM object itself.
class OutlookAddin:
***************
*** 247,250 ****
--- 385,396 ----
bars = activeExplorer.CommandBars
toolbar = bars.Item("Standard")
+ # Add our "Delete as ..." button
+ button = toolbar.Controls.Add(Type=constants.msoControlButton, Temporary=True)
+ # Hook events for the item
+ button.BeginGroup = True
+ button = DispatchWithEvents(button, ButtonDeleteAsEvent)
+ button.Init(self.manager, application, activeExplorer)
+ self.buttons.append(button)
+
# Add a pop-up menu to the toolbar
popup = toolbar.Controls.Add(Type=constants.msoControlPopup, Temporary=True)
***************
*** 323,326 ****
--- 469,494 ----
return new_hooks
+ 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:
+ win32ui.MessageBox("Please select a single item", "Large selection")
+ return None
+
+ ret = []
+ for i in range(sel.Count):
+ item = sel.Item(i+1)
+ if item.Class == constants.olMail:
+ msgstore_message = self.manager.message_store.GetMessage(item)
+ ret.append(msgstore_message)
+
+ if len(ret) == 0:
+ win32ui.MessageBox("No mail items are selected", "No selection")
+ return None
+ if allow_multi:
+ return ret
+ return ret[0]
+
def OnDisconnection(self, mode, custom):
print "SpamAddin - Disconnecting from Outlook"
***************
*** 331,336 ****
self.manager.Close()
self.manager = None
! self.buttons = None
!
print "Addin terminating: %d COM client and %d COM servers exist." \
% (pythoncom._GetInterfaceCount(), pythoncom._GetGatewayCount())
--- 499,506 ----
self.manager.Close()
self.manager = None
! if self.buttons:
! for button in self.buttons:
! button.Close()
! self.buttons = None
print "Addin terminating: %d COM client and %d COM servers exist." \
% (pythoncom._GetInterfaceCount(), pythoncom._GetGatewayCount())