www.pudn.com > clamwin-0.85.1-src.zip > OlAddin.py


#-----------------------------------------------------------------------------
# Name:        OlAddin.py
# Product:     WinClamAV Antivirus
#
# Author:      alch [alch at users dot sourceforge dot net]
#
# Created:     2004/31/03
# Copyright:   Copyright alch (c) 2004
# Licence:     
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
# 
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
# 
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.


#-----------------------------------------------------------------------------
# ClamWin Outlook Addin
# Parts of the code based on SpamBayes Source Code (Outlook2000/addin.py)
# Thanks to Sean True and Mark Hammond

# Copyright (C) 2002-2003 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:
# 
# PSF LICENSE AGREEMENT FOR THE SPAMBAYES PROJECT
# -----------------------------------------------
# 
# 1. This LICENSE AGREEMENT is between the Python Software Foundation
# ("PSF"), and the Individual or Organization ("Licensee") accessing and
# otherwise using the spambayes software ("Software") in source or binary
# form and its associated documentation.
# 
# 2. Subject to the terms and conditions of this License Agreement, PSF
# hereby grants Licensee a nonexclusive, royalty-free, world-wide
# license to reproduce, analyze, test, perform and/or display publicly,
# prepare derivative works, distribute, and otherwise use the Software
# alone or in any derivative version, provided, however, that PSF's
# License Agreement and PSF's notice of copyright, i.e., "Copyright (c)
# 2002-2003 Python Software Foundation; All Rights Reserved" are retained
# the Software alone or in any derivative version prepared by Licensee.
# 
# 3. In the event Licensee prepares a derivative work that is based on
# or incorporates the Software or any part thereof, and wants to make
# the derivative work available to others as provided herein, then
# Licensee hereby agrees to include in any such work a brief summary of
# the changes made to the Software.
# 
# 4. PSF is making the Software available to Licensee on an "AS IS"
# basis.  PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
# IMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
# DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
# FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
# INFRINGE ANY THIRD PARTY RIGHTS.
# 
# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
# SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
# A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING THE SOFTWARE,
# OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
# 
# 6. This License Agreement will automatically terminate upon a material
# breach of its terms and conditions.
# 
# 7. Nothing in this License Agreement shall be deemed to create any
# relationship of agency, partnership, or joint venture between PSF and
# Licensee.  This License Agreement does not grant permission to use PSF
# trademarks or trade name in a trademark sense to endorse or promote
# products or services of Licensee, or any third party.
# 
# 8. By copying, installing or otherwise using the Software, Licensee
# agrees to be bound by the terms and conditions of this License
# Agreement.

import SetUnicode
import sys
import os
import re
import tempfile
import warnings
import traceback
import _winreg
import Utils
import Config
import Process
import SplashScreen

# *sigh* - this is for the binary installer, and for the sake of one line
# that is implicit anyway, I gave up
import encodings

try:
    True, False
except NameError:
    # Maintain compatibility with Python 2.2
    True, False = 1, 0

# We have lots of locale woes.  The short story:
# * Outlook/MAPI will change the locale on us as some predictable
#   times - but also at unpredictable times.
# * Python currently insists on "C" locale - if it isn't, subtle things break,
#   such as floating point constants loaded from .pyc files.
# * Our config files also want a consistent locale, so periods and commas
#   are the same when they are read as when they are written.
# So, at a few opportune times, we simply set it back.
# We do it here as early as possible, before any imports that may see this
#
# See also [725466] Include a proper locale fix in Options.py,
# assorted errors relating to strange math errors, and spambayes-dev archives,
# starting July 23 2003.
import locale
locale.setlocale(locale.LC_NUMERIC, "C")

from win32com import universal
from win32com.server.exception import COMException
from win32com.client import gencache, DispatchWithEvents, Dispatch
import win32api
import pythoncom
from win32com.client import constants, getevents

import win32gui, win32con, win32clipboard # for button images!
from win32com.mapi import mapi, mapiutil, mapitags
import RedirectStd

_DEBUG=False

def dbg_print(*args):    
    if not _DEBUG:
        return
    else:
        print args
    
# As MarkH assumed, and later found to back him up in:
# http://www.slipstick.com/dev/comaddins.htm:
# On building add-ins for multiple Outlook versions, Randy Byrne writes in
# the microsoft.public.office.developer.com.add_ins newsgroup, "The best
# practice is to compile your Add-in with OL2000 and MSO9.dll. Then your
# add-in will work with both OL2000 and OL2002, and CommandBar events will
# work with both versions. If you need to use any specific OL2002 or
# Office 10.0 Object Library calls, you can use late binding to address
# those issues. The CommandBar Events are the same in both Office
# 2000 and Office XP."
# So that is what we do: specify the minimum versions of the typelibs we
# can work with - ie, Outlook 2000.

# win32com generally checks the gencache is up to date (typelib hasn't
# changed, makepy hasn't changed, etc), but when frozen we dont want to
# do this - not just for perf, but because they don't always exist!
bValidateGencache = not hasattr(sys, "frozen")
# Generate support so we get complete support including events
gencache.EnsureModule('{00062FFF-0000-0000-C000-000000000046}', 0, 9, 0,
                        bForDemand=True, bValidateFile=bValidateGencache) # Outlook 9
gencache.EnsureModule('{2DF8D04C-5BFA-101B-BDE5-00AA0044DE52}', 0, 2, 1,
                        bForDemand=True, bValidateFile=bValidateGencache) # Office 9
# We the "Addin Designer" typelib for its constants
gencache.EnsureModule('{AC0714F2-3D04-11D1-AE7D-00A0C90F26F4}', 0, 1, 0,
                        bForDemand=True, bValidateFile=bValidateGencache)
# ... and also for its _IDTExtensibility2 vtable interface.
universal.RegisterInterfaces('{AC0714F2-3D04-11D1-AE7D-00A0C90F26F4}', 0, 1, 0,
                             ["_IDTExtensibility2"])

try:
    from win32com.client import CastTo, WithEvents
except ImportError:
    print "*" * 50
    print "You appear to be running a win32all version pre 151, which is pretty old"
    print "I'm afraid it is time to upgrade"
    raise
# we seem to have all the COM support we need - let's rock!


# Button/Menu and other UI event handler classes
class ButtonEvent:
    def Init(self, handler, *args):
        self.handler = handler
        self.args = args
    def Close(self):
        self.handler = self.args = None
    def OnClick(self, button, cancel):
        # Callback from Outlook - locale may have changed.
        locale.setlocale(locale.LC_NUMERIC, "C") # see locale comments above
        self.handler(*self.args)

# no ui yet, no commands
def HelpAbout():
    try:                           
        curDir = Utils.GetCurrentDir(True)
        Utils.SpawnPyOrExe(os.path.join(curDir, 'ClamWin'), ' --mode=about')        
    except Exception, e:            
        win32gui.MessageBox(GetWindow(), 'An error occured while in ClamWin About Box.\n' + str(e), 'ClamWin', win32con.MB_OK | win32con.MB_ICONERROR)

# 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 -
    # 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 not os.path.isabs(fname):
        # images relative to the application path
        fname = os.path.join(Utils.GetCurrentDir(False),"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()
    
def GetWindow():
    hwnd = 0
    try:
        hwnd = win32gui.GetActiveWindow()
    except:
        hwnd = win32gui.GetForegroundWindow()   
    return hwnd        
    
class ScanError(Exception):
    def __init__(self, msg):
        Exception.__init__(self, msg)        

def ScanFile(path, config):        
    # initialise environment var TMPDIR
    # for clamav    
    try:
        if os.getenv('TMPDIR') is None:
            os.putenv('TMPDIR', 
                       re.sub('([A-Za-z]):[/\\\\]', r'/cygdrive/\1/', 
                       tempfile.gettempdir()).replace('\\', '/'))
        Utils.SetCygwinTemp()
    except Exception, e:
        print str(e)                    
            
        
    logfile = path + ' - Virus Deleted by ClamWin.txt'
    cmd = '--tempdir "%s"' % tempfile.gettempdir().replace('\\', '/').rstrip('/')

    cmd = '--max-ratio=0 --stdout --database="%s" --log="%s" "%s"' % \
            (config.Get('ClamAV', 'Database'), logfile, path)
         
    cmd = re.sub('([A-Za-z]):[/\\\\]', r'/cygdrive/\1/', cmd).replace('\\', '/')
    cmd = '"%s" %s' % (config.Get('ClamAV', 'ClamScan'), cmd)
                
    scanstatus = ''        
    retcode = 1
    proc = None
    try:        
        proc = Process.ProcessOpen(cmd)
        retcode = proc.wait()   
        dbg_print('scanning completed with %i' % retcode)  
        # returns 100 if a damaged rar archive was found
        if retcode >= 100 and retcode <= 106: 
            dbg_print('damaged archive - ignoring')  
            retcode = 0            
    except Exception, e:
        if proc is not None:
            proc.close()
        safe_remove(logfile)
        raise ScanError('An Error occured whilst starting clamscan: %s' % str(e))
        
    if proc is not None:
        proc.close()
    if retcode == 0:
        virusFound = False
    # check the retrun Code
    elif retcode == 1:
        virusFound = True
    else:
        # error, raise an exception
        try:
            error = file(logfile, 'rt').read()
            error = re.sub('/cygdrive/([A-Za-z])/', r'\1:/', error).replace('/', '\\')
            safe_remove(logfile)
        except Exception, e:
            raise ScanError('An Error occured reading clamscan report: %s' % str(e))
        raise ScanError('An Error occured whilst scanning:\n%s' % error)
                    
    # replace \n's with \r\n's
    # so it can be shown in notepad
    try:
        text = file(logfile, 'rt').read().replace('\n', '\r\n')        
        # remove leading path to a file name
        text = re.sub('.*/(.*\:)', '\\1', text)
        file(logfile, 'wt').write(text)
    except Exception, e:
        safe_remove(logfile)
        raise ScanError('An Error occured whilst converting clamscan report: %s' % str(e))    
    return (virusFound, logfile)
            
# returns 0 if everything is okay, or number fo infected files                     
def ScanMailItem(item, sending, added_attachments = None):     
    if not item.Attachments.Count:
        return 0
    
    config_file = os.path.join(Utils.GetProfileDir(True),'ClamWin.conf')
    if not os.path.isfile(config_file):
        config_file = 'ClamWin.conf'                    
    config = Config.Settings(config_file)    
    config.Read()
    
    # get the virus database version from daily.cvd
    # we will need it when deciding if the message should be rescanned;
    
    # after message is scanned the current daily.cvd version is saved;
    # then when message is next accessed we compare the saved version
    # with the current version and if they're different then 
    # we rescan the message
    virdb_ver = Utils.GetDBInfo(os.path.join(config.Get('ClamAV', 'Database'), 'daily.cvd'))[0]
    dbg_print('Daily.cvd Version: %s' % str(virdb_ver))
    
    dir = ''; path = ''; statusfile = ''        
    infected = []; attachments = []
    try:
        attachments = item.Attachments            
        
        # see if the message has already been scanned
        userProps = item.UserProperties    
        prop = userProps.Find('Scanned By ClamWin')
        if prop is not None:            
            if prop.Value == virdb_ver:
                dbg_print('ScanMailItem: Already Scanned')
                return 0
            
        waitCursor = WaitCursor()              
        for num in range(1, attachments.Count+1):
            att = attachments.Item(num)
            # bugfix [930909]
            # remove str(), was ausing unicode woes
            name = att.DisplayName            
            # normalise the filename so it contains alfanumeric characters only
            # and can be used as a filename                       
            if not name.isalnum():    
                new_name = ''
                for i in range(0, min(len(name), 255)):                    
                    if not name[i].isalnum():
                        new_name += '_'
                    else:
                        new_name += name[i]
            else:
                new_name = name    
            
            # create a temporary folder to save the attachment to
            dir = tempfile.mktemp()
            os.mkdir(dir)
            path = os.path.join(dir, new_name)
            dbg_print('ScanMailItem: saving attachment - ', path)
            try:
                att.SaveAsFile(path)
            except pythoncom.com_error, e:
                # ignore "Object not found" save errors
                # most likely the file won't be saved by outlook anyway
                hr, desc, exc, argErr = e                
                if exc[5] in (-2147221233, -2147024894, -2147467259): #0x8004010F, 0x80070002, 0x80040005
                    dbg_print('error saving attachment to %s. Error: %s' % (path, str(exc)))
                    safe_remove(dir)
                    continue
                else:
                    raise e
        
            code, statusfile = ScanFile(path, config)            
            # remove saved and scanned attachment  file
            safe_remove(path)                              
            if code == 0:            
                # no viruses found
                # remove the scan status file
                # along with temp dir
                safe_remove(statusfile, True)        
            else:                            
                # virus detected
                if sending:     
                    # for messages being sent display message box once and exit               
                    try:                    
                        msg = file(statusfile, 'rt').read()                                               
                        # remove the scan status file
                        # along with temp dir
                        safe_remove(statusfile, True)
                    except Exception, e:
                        msg = 'ClamWin could not scan file%s\n.Error: %s' % (statusfile, str(e))                        
                    win32gui.MessageBox(GetWindow(), msg, 'ClamWin Antivirus', win32con.MB_ICONERROR | win32con.MB_OK)
                    return 1
                else:
                    # bugfix [930909]
                    # remove str(att.DisplayName) - was causing unicode woes
                    infected.append((att, statusfile))

        # remove infected attachments            
        for info in infected:
            dbg_print('ScanMailItem: removing attachment - ', info[0].DisplayName)
            info[0].Delete()                            
            
        # add status files instead of the infected attachments
        # can't have it in the for loop above because in some cases
        # (like when a message is opened form the .msg file
        # outlook saves the message when you add the attachment
        # and screwes the indices            
        for info in infected:
            dbg_print('ScanMailItem: adding attachment - ', info[1])
            attachments.Add(Source=info[1], Type=constants.olByValue)                                
            # remove the scan status file
            # along with temp dir
            safe_remove(info[1], True)                   

        # add persistent property to the message
        # so we don't have to scan it in future
        # we save the daily.cvd version        
        # so message gets rescanned if database is updated
        if virdb_ver is not None:
            prop = userProps.Add('Scanned By ClamWin', constants.olNumber, False)
            prop.Value = virdb_ver
            dbg_print('ScanMailItem: Saving MailItem')
            try:
	             item.Save()           
            except pythoncom.com_error, e:
                # read only message store (hotmail, etc)
                # ignore save errors
                hr, desc, exc, argErr = e
                if hr != -2147352567:
                    raise e
        
        if len(infected) > 0:                        
            # for Outlook 2000 we need to display a message box in order to 
            # warn a user becuase it will not change the attachments info in
            # the event handlers
            if int(item.Application.Version.split('.', 1)[0]) < 10:                
                msg = 'ClamWin Antivirus has detected a virus in the message attachments!'
                win32gui.MessageBox(GetWindow(), msg, 'ClamWin Found A Virus!', win32con.MB_ICONERROR | win32con.MB_OK)                            
            elif config.Get('UI', 'TrayNotify') == '1':       
                # show balloon in outlook 2002 +
                tray_notify_params = (('Virus has been detected in an email attachment! The attachment was replaced with the report file.', 1, 
                                win32gui.NIIF_ERROR, 30000),
                ('An error occured whilst scanning email message.', 0, 
                win32gui.NIIF_WARNING, 30000))
                Utils.ShowBalloon(1, tray_notify_params)

            return len(infected)
        else:
            return 0                         
    except Exception, e:
        # cleanup any created files and folders
        safe_remove(path)
        safe_remove(statusfile)                
        for info in infected:
            safe_remove(info[1], True) 
        safe_remove(dir)   
        
        # display error   
        win32gui.MessageBox(GetWindow(), str(e), 'ClamWin', win32con.MB_OK | win32con.MB_ICONERROR)        
        return True        

    return len(infected)

def safe_remove(path, removeLastDir = False):
    try:
        if os.path.isfile(path):
            os.remove(path)  
        else:
            os.rmdir(path)  
        if removeLastDir:
            dir = os.path.split(path)[0]              
            if os.path.exists(dir):
                os.rmdir(dir)              
    except Exception, e:
        print 'Could not remove file: %s. Error: %s' % (path, str(e))

class WaitCursor:    
    def __init__(self):
        self._hCursor = win32gui.SetCursor(win32gui.LoadCursor(0, win32con.IDC_WAIT))    
    def __del__(self):
        win32gui.SetCursor(self._hCursor)    

# Base Class for Explorer, Inspector and MailItem Outlook Objects           
class ObjectWithEvents:
    def Init(self, collection):    
        self.collection = collection
        
    def OnClose(self):      
        if hasattr(self.collection.host, 'DisconnectEventHandler'):
            self.collection.host.DisconnectEventHandler(self.collection)   
        self.collection._DoDeadObject(self)
        self.collection = None        
        self.close()         
        # disconnect events.

# EventSink Base Class for Explorers, Inspectors and MailItems Outlook Object Collections            
class ObjectsEvent:
    def Init(self, classWithEvents, host):
        self.objects = []        
        self.classWithEvents = classWithEvents        
        # host object that created EventSink
        self.host = host

    def Close(self):        
        while self.objects:
            self._DoDeadObject(self.objects[0])
        self.objects = None
        self.close()

    def _DoNewObject(self, obj):
        obj = DispatchWithEvents(obj, self.classWithEvents)
        obj.Init(self)
        self.objects.append(obj)
        return obj

    def _DoDeadObject(self, obj):        
        self.objects.remove(obj)
        obj = None        
            
        
        
# 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(ObjectWithEvents):
    def Init(self, explorers_collection):
        dbg_print('ExplorerWithEvents:Init')
        self.have_setup_ui = False        
        self.event_handlers = []                
        ObjectWithEvents.Init(self, explorers_collection)

    def SetupUI(self):               
        # find Help->About Outlook menu
        aboutOutlook = self.CommandBars.FindControl(
                            Type = constants.msoControlButton,
                            Id = 927)
        
        if aboutOutlook is None:
            return
                
        popup = aboutOutlook.Parent
        if popup is None:
            return
        # Add Help->About Clamwin menu item 
        child = self._AddControl(popup,
                       constants.msoControlButton,
                       ButtonEvent, (HelpAbout, ),
                       Caption="&About ClamWin Antivirus",
                       TooltipText = "Shows the ClamWin About Box",
                       Enabled = True,
                       Visible=True,
                       Tag = "ClamWin.About")
        self.have_setup_ui = True
                           
    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.
        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)
        if item is None:
            # Now add the item itself to the parent.
            try:
                item = parent.Controls.Add(Type=control_type, Temporary=True)
            except pythoncom.com_error, e:               
                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)
            # 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.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.collection.button_event_map[tag] = item
        return item

    # The Outlook event handlers
    def OnActivate(self): 
        dbg_print('ExplorerWithEvents:OnActivate')
        # 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):         
        dbg_print('ExplorerWithEvents:OnSelectionChange')
        # See comments for OnNewExplorer below.
        if not self.have_setup_ui:
            self.SetupUI()
        if self.IsPaneVisible(constants.olPreview) and \
            self.Selection.Count == 1:            
            item = self.Selection.Item(1)
            if item.Class == constants.olMail and item.Sent:                                                  
                ScanMailItem(item, False)                       
        
    def OnClose(self):                                
        dbg_print('ExplorerWithEvents:OnClose')
        for event_handler in self.event_handlers:        
            event_handler.Close()
        self.event_handler = []        
        ObjectWithEvents.OnClose(self)

    def OnBeforeFolderSwitch(self, new_folder, cancel):
        dbg_print('ExplorerWithEvents:OnBeforeFolderSwitch')
        pass

    def OnFolderSwitch(self):        
        # Yet another work-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.
        dbg_print('ExplorerWithEvents:OnFolderSwitch')
        if not self.have_setup_ui:
            self.SetupUI()

    def OnBeforeViewSwitch(self, new_view, cancel):
        dbg_print('ExplorerWithEvents:OnBeforeViewSwitch')
        pass

    def OnViewSwitch(self):        
        dbg_print('ExplorerWithEvents:OnViewSwitch')
        if not self.have_setup_ui:
            self.SetupUI()
            
    

# Events from our "Explorers" collection (not an Explorer instance)
class ExplorersEvent(ObjectsEvent):
    def Init(self, olAddin):
        dbg_print('ExplorersEvent:Init')
        self.button_event_map = {}
        ObjectsEvent.Init(self, ExplorerWithEvents, olAddin)    

    def _DoDeadObject(self, obj):
        dbg_print('ExplorersEvent:_DoDeadObject')
        ObjectsEvent._DoDeadObject(self, obj)
        if len(self.objects)==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!        
        dbg_print('ExplorersEvent:OnNewExplorer')
        self._DoNewObject(explorer)

# A class that manages an "Outlook Inspector" - that is, a message or contact window
class InspectorWithEvents(ObjectWithEvents):
    def Init(self, inspectors_collection):               
        dbg_print('InspectorWithEvents:Init')
        self.event_handlers = []        
        item = self.CurrentItem        
        if item.Class == constants.olMail:                                            
            # Create EventHandler for MailItem
            mailitem_events = WithEvents(item, MailItemsEvent)
            mailitem_events.Init(self)        
            mailitem_events._DoNewObject(item)            
            self.event_handlers.append(mailitem_events)            
        ObjectWithEvents.Init(self, inspectors_collection)
    
    # The Outlook event handlers
    def OnActivate(self):
        dbg_print('InspectorWithEvents:OnActivate')
        pass                                                     
        
    def OnClose(self):     
        dbg_print('InspectorWithEvents:OnClose')
        for handler in self.event_handlers:        
            handler.Close()
        self.event_handlers = []
        ObjectWithEvents.OnClose(self)
        
    def DisconnectEventHandler(self, obj):
        dbg_print('InspectorWithEvents:DisconnectEventHandler')
        obj.close()
        self.event_handlers.remove(obj)      
        
# Events from our "Inspectors" collection
class InspectorsEvent(ObjectsEvent):
    def Init(self, host):          
        dbg_print('InspectorsEvent:Init')
        ObjectsEvent.Init(self, InspectorWithEvents, host)  
        
    def OnNewInspector(self, inspector):   
        dbg_print('InspectorsEvent:OnNewInspector')
        self._DoNewObject(inspector)  
        

# A class that manages an "Outlook MailItem" - that is, a message 
class MailItemWithEvents(ObjectWithEvents):
    def Init(self, items_collection):      
        dbg_print('MailItemWithEvents:Init')
        ObjectWithEvents.Init(self, items_collection)
        self._scanned = False
        self._close_inspector = False
        self._num_infected = 0
        
    def OnClose(self, cancel):      
        dbg_print('MailItemWithEvents:OnClose')
        host = self.collection.host
        ObjectWithEvents.OnClose(self)
        if self._close_inspector:
            # need to disconnect Inspectors Collection ebent handler because
            # Outlook 2000 doesn't fire Inspector:Close event
            # for sent messages and remains hanging around after exit
            dbg_print('MailItemWithEvents:OnClose host.OnClose()')                    
            host.OnClose()
        
        

    def OnRead(self):  
        dbg_print('MailItemWithEvents:OnRead')
        # OnOpen event is not always fired
        # only when a user double-clicks the message
        # so we scan here and reinitialize attachmets
        if self.Sent and not self._scanned:             
            dbg_print('MailItemWithEvents:OnRead scanning')
            self._scanned = True
            self._num_infected = ScanMailItem(self, False)  
            
        
    def OnOpen(self, cancel):  
        dbg_print('MailItemWithEvents:OnOpen')
        if not self._scanned and self.Sent :      
            # in case OnRead has not been called
            dbg_print('MailItemWithEvents:OnOpen scanning')
            self._scanned = True
            self._num_infected = ScanMailItem(self, False)  
        elif  self._num_infected:
            # a bit of trickery here - remove and reinsert attachments 
            # so that bloody outlook shows our changes
            dbg_print('MailItemWithEvents:OnOpen Scanned Earlier')
            try:
                saved_attachments = []            
                for i in range(self.Attachments.Count - self._num_infected + 1, self.Attachments.Count + 1):
                    att = self.Attachments.Item(i)
                    # save attachment to a temp file
                    name = att.DisplayName
                    dir = tempfile.mktemp()
                    os.mkdir(dir)
                    filename = os.path.join(dir, name)
                    att.SaveAsFile(filename)
                    saved_attachments.append((att, filename))
            
                for saved in saved_attachments:
                    saved[0].Delete()
                
                for saved in saved_attachments:
                    self.Attachments.Add(Source=saved[1], Type=constants.olByValue)
                for saved in saved_attachments:
                    safe_remove(saved[1], True)                
                saved_attachments = []    
                
                try:
                   self.Save()           
                except pythoncom.com_error, e:
                    # read only message store (hotmail, etc
                    # ignore save errors
                    hr, desc, exc, argErr = e
                    if hr != -2147352567:
                        raise e                    
            except Exception, e:
                for saved in saved_attachments:
                    safe_remove(saved[1], True)
                msg = 'ClamWin could not replace an attachment. Error: %s' % str(e)
                win32gui.MessageBox(GetWindow(), msg, 'ClamWin Antivirus!', win32con.MB_ICONERROR | win32con.MB_OK)                                                                                      
    
    def OnWrite(self, cancel):
        dbg_print('MailItemWithEvents:OnWrite')
        if self.Sent and not self._scanned:
            prop = self.UserProperties.Find('Scanned By ClamWin')
            if prop is not None:
                prop.Delete()
                                
    def OnSend(self, cancel):        
        dbg_print('MailItemWithEvents:OnSend')
        virus_found = (ScanMailItem(self, True) > 0)
        cancel = virus_found
        # disconnect Inspectors Collection event handler
        # in OnClose
        self._close_inspector = True
        return not cancel
        

# Events from our "MailItems" collection
class MailItemsEvent(ObjectsEvent):
    def Init(self, host):  
        dbg_print('MailItemsEvent:Init')
        ObjectsEvent.Init(self, MailItemWithEvents, host)  
        
    def OnItemAdd(self, mailitem):               
        dbg_print('MailItemsEvent:OnItemAdd')
        self._DoNewObject(mailitem)  
                    
# The outlook Plugin COM object itself.
class OutlookAddin(ObjectsEvent):
    _com_interfaces_ = ['_IDTExtensibility2']
    _public_methods_ = []
#    _reg_clsctx_ = pythoncom.CLSCTX_INPROC_SERVER
    _reg_clsid_ = "{E77FA584-1433-4af3-800D-AEC49BCCCB11}"
    _reg_progid_ = "ClamWin.OutlookAddin"
    _reg_policy_spec_ = "win32com.server.policy.EventHandlerPolicy"

    def __init__(self):        
        self.application = None       
        # check the debug flag iun the registry
        global _DEBUG
        try:
            subkey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, 'Software\\ClamWin')            
            _DEBUG = int(_winreg.QueryValueEx(subkey, "Debug")[0])==1            
        except:
            _DEBUG = False                

    def OnConnection(self, application, connectMode, addin, custom):            
        dbg_print('OutlookAddin:OnConnection')
        # Handle failures during initialization so that we are not
        # automatically disabled by Outlook.
        locale.setlocale(locale.LC_NUMERIC, "C") # see locale comments above
        try:
            self.application = application
            self.event_handlers = [] # 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()
            print "There was an error initializing the ClamWin addin\r\n\r\n"\
                "Please re-start Outlook and try again."

    def OnStartupComplete(self, custom):
        dbg_print('OutlookAddin:OnStartupComplete')
        Utils.CreateProfile()
        # display SplashScreen
        try:            
            splash = os.path.join(Utils.GetCurrentDir(False), "img\\Splash.bmp")
            SplashScreen.ShowSplashScreen(splash, 5)
        except Exception, e:           
            print "An error occured whilst displaying the spashscreen %s. Error: %s." % (splash, str(e))
        # 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.
        # Toolbar and other UI stuff must be setup once startup is complete.
        explorers = self.application.Explorers
        # and Explorers events so we know when new explorers spring into life.
        explorers_events = WithEvents(explorers, ExplorersEvent)
        explorers_events.Init(self)        
        # And hook our UI elements to all existing explorers
        for i in range(explorers.Count):
            explorer = explorers.Item(i+1)
            explorer = explorers_events._DoNewObject(explorer)
            explorer.OnFolderSwitch()
        self.event_handlers.append(explorers_events)
        
        inspectors = self.application.Inspectors
        # and Inspectors events so we know when new inspectors spring into life.
        inspectors_events = WithEvents(inspectors, InspectorsEvent)
        inspectors_events.Init(self)        
        # And elements to all existing inspectors
        for i in range(inspectors.Count):
            inspector = inspectors.Item(i+1)
            inspector = inspectors_events._DoNewObject(inspector)
        self.event_handlers.append(inspectors_events)
        # release application object otherwise Outlook2003 won't
        # shutdown if we are running in out-of-process EXE
        self.application = None    
        
    def OnDisconnection(self, mode, custom):
        dbg_print('ClamWin - Disconnecting from Outlook')
        for handler in self.event_handlers:        
            handler.Close()
        self.event_handlers = []        
        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):
        dbg_print('OutlookAddin:OnBeginShutdown')
        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, "ClamWin Antivirus")
    _winreg.SetValueEx(subkey, "FriendlyName", 0, _winreg.REG_SZ, "ClamWin Antivirus")

# 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'
# and hkey_current_user
def DllInstall(bInstall, cmdline):
    klass = OutlookAddin
    if bInstall:
        # Unregister the old installation, if one exists.
        DllUnregisterServer()
        rootkey = None
        if cmdline.lower().find('hkey_local_machine')>=0:                    
            rootkey = _winreg.HKEY_LOCAL_MACHINE
            print "Registering (in HKEY_LOCAL_MACHINE)..."
        elif cmdline.lower().find('hkey_current_user')>=0:                    
            rootkey = _winreg.HKEY_CURRENT_USER
            print "Registering (in HKEY_CURRENT_USER)..."
        if rootkey is not None:
            # Don't catch exceptions here - if it fails, the Dll registration
            # must fail.
            _DoRegister(klass, _winreg.HKEY_LOCAL_MACHINE)
            print "Registration 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__':
    if '--register' in sys.argv[1:] or \
       '--unregister' in sys.argv[1:] or \
       not hasattr(sys, "frozen"):
        import win32com.server.register
        win32com.server.register.UseCommandLine(OutlookAddin)    
        if "--unregister" in sys.argv[1:]:
            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')
    elif hasattr(sys, "frozen"):
        if sys.frozen == "exe":
            # start the exe server.
            from win32com.server import localserver
            localserver.main()


##        #Some MAPI code, just in case we need it 
##        try:
##            iMsg = self.MAPIOBJECT.QueryInterface(mapi.IID_IMessage)
##            for num in range (0, self.Attachments.Count):                        
##                print "Deleting Attachment - ", num
##                iMsg.DeleteAttach(num, 0, None, 0)
##                deleted = True
##            if deleted:
##                iMsg.SaveChanges(0)
##            iMsg = None
##        except Exception, e:
##            print "MailItem OnRead Exception: ", str(e)

##for i in range(attachments.Count - num_infected+1, attachments.Count+1):
##    try:            
##        iMsg = item.MAPIOBJECT.QueryInterface(mapi.IID_IMessage)                                
##        print 'getting att', i               
##        att = attachments.Item(i)
##        print 'got att'
##        iAtt = att.MAPIOBJECT.QueryInterface(mapi.IID_IMAPIProp)                                
##        print 'got iAtt'
##        iAtt.SetProps([(mapitags.PR_ATTACH_FILENAME, att.DisplayName),])
##        print 'set PR_ATTACH_FILENAME'
##        iAtt.SetProps([(mapitags.PR_ATTACH_LONG_FILENAME, att.DisplayName),])
##        print 'set PR_ATTACH_LONG_FILENAME'
##        iAtt.SaveChanges(mapi.KEEP_OPEN_READWRITE)
##        print 'Saved Changes'
##        iAtt = None; att = None
##    except Exception, e:
##        print "Could not set attachment display name: ", str(e)
##