from __future__ import generators

import sys, os, re
import locale

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

# MAPI imports etc.
from win32com.client import Dispatch, constants
from win32com.mapi import mapi, mapiutil
from win32com.mapi.mapitags import *
import pythoncom
import winerror

try:
    PR_USERFIELDS # only in new win32all
except NameError:
    PR_USERFIELDS = 0x36E30102 # PROP_TAG(PT_BINARY, 0x36e3)

# Additional MAPI constants we dont have in Python
MESSAGE_MOVE = 0x1 # from MAPIdefs.h
MSGFLAG_READ = 0x1 # from MAPIdefs.h
MSGFLAG_UNSENT = 0x00000008

MYPR_BODY_HTML_A = 0x1013001e # magic <wink>
MYPR_BODY_HTML_W = 0x1013001f # ditto

CLEAR_READ_FLAG = 0x00000004
CLEAR_RN_PENDING = 0x00000020
CLEAR_NRN_PENDING = 0x00000040
SUPPRESS_RECEIPT = 0x1

USE_DEFERRED_ERRORS = mapi.MAPI_DEFERRED_ERRORS # or set to zero to see what changes <wink>

#import warnings
#if sys.version_info >= (2, 3):
#    # sick off the new hex() warnings!
#    warnings.filterwarnings("ignore", category=FutureWarning, append=1)

# Nod to our automated test suite.  Currently supports a hack so our test
# message is filtered, and also for raising exceptions at key times.
# see tester.py for more details.
test_suite_running = False
test_suite_failure_request = None
test_suite_failure = None
# Set to the number of times we should fail, or None for all times.
test_suite_failure_count = None
# Sometimes the test suite will request that we simulate MAPI errors.
def help_test_suite(checkpoint_name):
    global test_suite_failure_request, test_suite_failure_count
    if test_suite_running and \
       test_suite_failure_request == checkpoint_name:
        if test_suite_failure_count:
            test_suite_failure_count -= 1
            if test_suite_failure_count==0:
                test_suite_failure_request = None
        raise test_suite_failure[0], test_suite_failure[1]

# Exceptions raised by this module.  Raw MAPI exceptions should never
# be raised to the caller.
class MsgStoreException(Exception):
    def __init__(self, mapi_exception, extra_msg = None):
        self.mapi_exception = mapi_exception
        self.extra_msg = extra_msg
        Exception.__init__(self, mapi_exception, extra_msg)
    def __str__(self):
        try:
            if self.mapi_exception is not None:
                err_str = GetCOMExceptionString(self.mapi_exception)
            else:
                err_str = self.extra_msg or ''
            return "%s: %s" % (self.__class__.__name__, err_str)
         # Python silently consumes exceptions here, and uses
         # <unprintable object>
        except:
            print "FAILED to str() a MsgStore exception!"
            import traceback
            traceback.print_exc()

# Exception raised when you attempt to get a message or folder that doesn't
# exist.  Usually means you are querying an ID that *was* valid, but has
# since been moved or deleted.
# Note you may get this exception "getting" objects (such as messages or
# folders), or accessing properties once the object was created (the message
# may be moved under us at any time)
class NotFoundException(MsgStoreException):
    pass

# Exception raised when you try and modify a "read only" object.
# Only currently examples are Hotmail and IMAP folders.
class ReadOnlyException(MsgStoreException):
    pass

# The object has changed since it was opened.
class ObjectChangedException(MsgStoreException):
    pass

# Utility functions for exceptions.  Convert a COM exception to the best
# manager exception.
def MsgStoreExceptionFromCOMException(com_exc):
    if IsNotFoundCOMException(com_exc):
        return NotFoundException(com_exc)
    if IsReadOnlyCOMException(com_exc):
        return ReadOnlyException(com_exc)
    scode = NormalizeCOMException(com_exc)[0]
    # And simple scode based ones.
    if scode == mapi.MAPI_E_OBJECT_CHANGED:
        return ObjectChangedException(com_exc)
    return MsgStoreException(com_exc)

def NormalizeCOMException(exc_val):
    hr, msg, exc, arg_err = exc_val
    if hr == winerror.DISP_E_EXCEPTION and exc:
        # 'client' exception - unpack 'exception object'
        wcode, source, msg, help1, help2, hr = exc
    return hr, msg, exc, arg_err

# Build a reasonable string from a COM exception tuple
def GetCOMExceptionString(exc_val):
    hr, msg, exc, arg_err = NormalizeCOMException(exc_val)
    err_string = mapiutil.GetScodeString(hr)
    return "Exception 0x%x (%s): %s" % (hr, err_string, msg)

# Does this exception probably mean "object not found"?
def IsNotFoundCOMException(exc_val):
    hr, msg, exc, arg_err = NormalizeCOMException(exc_val)
    return hr in [mapi.MAPI_E_OBJECT_DELETED, mapi.MAPI_E_NOT_FOUND]

# Does this exception probably mean "object not available 'cos you ain't logged
# in, or 'cos the server is down"?
def IsNotAvailableCOMException(exc_val):
    hr, msg, exc, arg_err = NormalizeCOMException(exc_val)
    return hr == mapi.MAPI_E_FAILONEPROVIDER

def IsReadOnlyCOMException(exc_val):
    # This seems to happen for IMAP mails (0x800cccd3)
    # and also for hotmail messages (0x8004dff7)
    known_failure_codes = -2146644781, -2147164169
    exc_val = NormalizeCOMException(exc_val)
    return exc_val[0] in known_failure_codes

def ReportMAPIError(manager, what, exc_val):
    hr, exc_msg, exc, arg_err = exc_val
    if hr == mapi.MAPI_E_TABLE_TOO_BIG:
        err_msg = what + " failed as one of your\r\n" \
                    "Outlook folders is full.  Futher operations are\r\n" \
                    "likely to fail until you clean up this folder.\r\n\r\n" \
                    "This message will not be reported again until SpamBayes\r\n"\
                    "is restarted."
    else:
        err_msg = what + " failed due to an unexpected Outlook error.\r\n" \
                  + GetCOMExceptionString(exc_val) + "\r\n\r\n" \
                  "It is recommended you restart Outlook at the earliest opportunity\r\n\r\n" \
                  "This message will not be reported again until SpamBayes\r\n"\
                  "is restarted."
    manager.ReportErrorOnce(err_msg)

# Our objects.
class MAPIMsgStore:
    # Stash exceptions in the class for ease of use by consumers.
    MsgStoreException = MsgStoreException
    NotFoundException = NotFoundException
    ReadOnlyException = ReadOnlyException
    ObjectChangedException = ObjectChangedException

    def __init__(self, outlook = None):
        self.outlook = outlook
        cwd = os.getcwd() # remember the cwd - mapi changes it under us!
        mapi.MAPIInitialize(None)
        logonFlags = (mapi.MAPI_NO_MAIL |
                      mapi.MAPI_EXTENDED |
                      mapi.MAPI_USE_DEFAULT)
        self.session = mapi.MAPILogonEx(0, None, None, logonFlags)
        # Note that if the CRT still has a default "C" locale, MAPILogonEx()
        # will change it.  See locale comments in addin.py
        locale.setlocale(locale.LC_NUMERIC, "C")
        self.mapi_msg_stores = {}
        self.default_store_bin_eid = None
        os.chdir(cwd)

    def Close(self):
        self.mapi_msg_stores = None
        self.session.Logoff(0, 0, 0)
        self.session = None
        mapi.MAPIUninitialize()

    def GetProfileName(self):
        # Return the name of the MAPI profile currently in use.
        # XXX - note - early win32all versions are missing
        # GetStatusTable :(
        try:
            self.session.GetStatusTable
        except AttributeError:
            # We try and recover from this when win32all is updated, so no need to whinge.
            return None

        MAPI_SUBSYSTEM = 39
        restriction = mapi.RES_PROPERTY, (mapi.RELOP_EQ, PR_RESOURCE_TYPE,
                                          (PR_RESOURCE_TYPE, MAPI_SUBSYSTEM))
        table = self.session.GetStatusTable(0)
        rows = mapi.HrQueryAllRows(table,
                                    (PR_DISPLAY_NAME_A,),   # columns to retrieve
                                    restriction,     # only these rows
                                    None,            # any sort order is fine
                                    0)               # any # of results is fine
        assert len(rows)==1, "Should be exactly one row"
        (tag, val), = rows[0]
        # I can't convince MAPI to give me the Unicode name, so we assume
        # encoded as MBCS.
        return val.decode("mbcs", "ignore")

    def _GetMessageStore(self, store_eid): # bin eid.
        try:
            # Will usually be pre-fetched, so fast-path out
            return self.mapi_msg_stores[store_eid]
        except KeyError:
            pass
        given_store_eid = store_eid
        if store_eid is None:
            # Find the EID for the default store.
            tab = self.session.GetMsgStoresTable(0)
            # Restriction for the table:  get rows where PR_DEFAULT_STORE is true.
            # There should be only one.
            restriction = (mapi.RES_PROPERTY,   # a property restriction
                           (mapi.RELOP_EQ,      # check for equality
                            PR_DEFAULT_STORE,   # of the PR_DEFAULT_STORE prop
                            (PR_DEFAULT_STORE, True))) # with True
            rows = mapi.HrQueryAllRows(tab,
                                       (PR_ENTRYID,),   # columns to retrieve
                                       restriction,     # only these rows
                                       None,            # any sort order is fine
                                       0)               # any # of results is fine
            # get first entry, a (property_tag, value) pair, for PR_ENTRYID
            row = rows[0]
            eid_tag, store_eid = row[0]
            self.default_store_bin_eid = store_eid

        # Open it.
        store = self.session.OpenMsgStore(
                                0,      # no parent window
                                store_eid,    # msg store to open
                                None,   # IID; accept default IMsgStore
                                # need write access to add score fields
                                mapi.MDB_WRITE |
                                    # we won't send or receive email
                                    mapi.MDB_NO_MAIL |
                                    USE_DEFERRED_ERRORS)
        # cache it
        self.mapi_msg_stores[store_eid] = store
        if given_store_eid is None: # The default store
            self.mapi_msg_stores[None] = store
        return store

    def GetRootFolder(self, store_id = None):
        # if storeID is None, gets the root folder from the default store.
        store = self._GetMessageStore(store_id)
        hr, data = store.GetProps((PR_ENTRYID, PR_IPM_SUBTREE_ENTRYID), 0)
        store_eid = data[0][1]
        subtree_eid = data[1][1]
        eid = mapi.HexFromBin(store_eid), mapi.HexFromBin(subtree_eid)
        return self.GetFolder(eid)

    def _OpenEntry(self, id, iid = None, flags = None):
        # id is already normalized.
        store_id, item_id = id
        store = self._GetMessageStore(store_id)
        if flags is None:
            flags = mapi.MAPI_MODIFY | USE_DEFERRED_ERRORS
        return store.OpenEntry(item_id, iid, flags)

    # Normalize an "external" hex ID to an internal binary ID.
    def NormalizeID(self, item_id):
        assert type(item_id)==type(()), \
               "Item IDs must be a tuple (not a %r)" % item_id
        try:
            store_id, entry_id = item_id
            return mapi.BinFromHex(store_id), mapi.BinFromHex(entry_id)
        except ValueError:
            raise MsgStoreException(None, "The specified ID '%s' is invalid" % (item_id,))

    def _GetSubFolderIter(self, folder):
        table = folder.GetHierarchyTable(0)
        rows = mapi.HrQueryAllRows(table,
                                   (PR_ENTRYID, PR_STORE_ENTRYID, PR_DISPLAY_NAME_A),
                                   None,
                                   None,
                                   0)
        for (eid_tag, eid), (store_eid_tag, store_eid), (name_tag, name) in rows:
            item_id = store_eid, eid
            sub = self._OpenEntry(item_id)
            table = sub.GetContentsTable(0)
            yield MAPIMsgStoreFolder(self, item_id, name, table.GetRowCount(0))
            for store_folder in self._GetSubFolderIter(sub):
                yield store_folder

    def GetFolderGenerator(self, folder_ids, include_sub):
        for folder_id in folder_ids:
            try:
                folder_id = self.NormalizeID(folder_id)
            except MsgStoreException, details:
                print "NOTE: Skipping invalid folder", details
                continue
            try:
                folder = self._OpenEntry(folder_id)
                table = folder.GetContentsTable(0)
            except pythoncom.com_error, details:
                # We will ignore *all* such errors for the time
                # being, but give verbose details for results we don't
                # know about
                if IsNotAvailableCOMException(details):
                    print "NOTE: Skipping folder for this session - temporarily unavailable"
                elif IsNotFoundCOMException(details):
                    print "NOTE: Skipping deleted folder"
                else:
                    print "WARNING: Unexpected MAPI error opening folder"
                    print GetCOMExceptionString(details)
                continue
            rc, props = folder.GetProps( (PR_DISPLAY_NAME_A,), 0)
            yield MAPIMsgStoreFolder(self, folder_id, props[0][1],
                                     table.GetRowCount(0))
            if include_sub:
                for f in self._GetSubFolderIter(folder):
                    yield f

    def GetFolder(self, folder_id):
        # Return a single folder given the ID.
        try: # catch all MAPI errors
            try:
                # See if this is an Outlook folder item
                sid = mapi.BinFromHex(folder_id.StoreID)
                eid = mapi.BinFromHex(folder_id.EntryID)
                folder_id = sid, eid
            except AttributeError:
                # No 'EntryID'/'StoreID' properties - a 'normal' ID
                folder_id = self.NormalizeID(folder_id)
            folder = self._OpenEntry(folder_id)
            table = folder.GetContentsTable(0)
            # Ensure we have a long-term ID.
            rc, props = folder.GetProps( (PR_ENTRYID, PR_DISPLAY_NAME_A), 0)
            folder_id = folder_id[0], props[0][1]
            return MAPIMsgStoreFolder(self, folder_id, props[1][1],
                                  table.GetRowCount(0))
        except pythoncom.com_error, details:
            raise MsgStoreExceptionFromCOMException(details)

    def GetMessage(self, message_id):
        # Return a single message given either the ID, or an Outlook
        # message representing the object.
        try: # catch all MAPI exceptions.
            try:
                eid = mapi.BinFromHex(message_id.EntryID)
                sid = mapi.BinFromHex(message_id.Parent.StoreID)
                message_id = sid, eid
            except AttributeError:
                # No 'EntryID'/'StoreID' properties - a 'normal' ID
                message_id = self.NormalizeID(message_id)
            mapi_object = self._OpenEntry(message_id)
            hr, data = mapi_object.GetProps(MAPIMsgStoreMsg.message_init_props,0)
            return MAPIMsgStoreMsg(self, data)
        except pythoncom.com_error, details:
            raise MsgStoreExceptionFromCOMException(details)

    def YieldReceiveFolders(self, msg_class = "IPM.Note"):
        # Get the main receive folder for each message store.
        tab = self.session.GetMsgStoresTable(0)
        rows = mapi.HrQueryAllRows(tab,
                                    (PR_ENTRYID,),   # columns to retrieve
                                    None,            # all rows
                                    None,            # any sort order is fine
                                    0)               # any # of results is fine
        for row in rows:
            # get first entry, a (property_tag, value) pair, for PR_ENTRYID
            eid_tag, store_eid = row[0]
            try:
                store = self._GetMessageStore(store_eid)
                folder_eid, ret_class = store.GetReceiveFolder(msg_class, 0)
                hex_folder_eid = mapi.HexFromBin(folder_eid)
                hex_store_eid = mapi.HexFromBin(store_eid)
            except pythoncom.com_error, details:
                if not IsNotAvailableCOMException(details):
                    print "ERROR enumerating a receive folder -", details
                continue
            try:
                folder = self.GetFolder((hex_store_eid, hex_folder_eid))
                # For 'unconfigured' stores, or "stand-alone" PST files,
                # this is a root folder - so not what we wan't.  Only return
                # folders with a parent.
                if folder.GetParent() is not None:
                    yield folder
            except MsgStoreException, details:
                print "ERROR opening receive folder -", details
                # but we just continue
                continue

_MapiTypeMap = {
    type(0.0): PT_DOUBLE,
    type(0): PT_I4,
    type(''): PT_STRING8,
    type(u''): PT_UNICODE,
    # In Python 2.2.2, bool isn't a distinct type (type(1==1) is type(0)).
#    type(1==1): PT_BOOLEAN,
}

def GetPropFromStream(mapi_object, prop_id):
    try:
        stream = mapi_object.OpenProperty(prop_id,
                                          pythoncom.IID_IStream,
                                          0, 0)
        chunks = []
        while 1:
            chunk = stream.Read(4096)
            if not chunk:
                break
            chunks.append(chunk)
        return "".join(chunks)
    except pythoncom.com_error, d:
        print "Error getting property", mapiutil.GetPropTagName(prop_id), \
              "from stream:", d
        return ""

def GetPotentiallyLargeStringProp(mapi_object, prop_id, row):
    got_tag, got_val = row
    if PROP_TYPE(got_tag) == PT_ERROR:
        ret = ""
        if got_val == mapi.MAPI_E_NOT_FOUND:
            pass # No property for this message.
        elif got_val == mapi.MAPI_E_NOT_ENOUGH_MEMORY:
            # Too big for simple properties - get via a stream
            ret = GetPropFromStream(mapi_object, prop_id)
        else:
            tag_name = mapiutil.GetPropTagName(prop_id)
            err_string = mapiutil.GetScodeString(got_val)
            print "Warning - failed to get property %s: %s" % (tag_name,
                                                                err_string)
    else:
        ret = got_val
    return ret

# Some nasty stuff for getting RTF out of the message
def GetHTMLFromRTFProperty(mapi_object, prop_tag = PR_RTF_COMPRESSED):
    try:
        rtf_stream = mapi_object.OpenProperty(prop_tag, pythoncom.IID_IStream,
                                              0, 0)
        html_stream = mapi.WrapCompressedRTFStream(rtf_stream, 0)
        html = mapi.RTFStreamToHTML(html_stream)
    except pythoncom.com_error, details:
        if not IsNotFoundCOMException(details):
            print "ERROR getting RTF body", details
        return ""
    # html may be None if RTF not originally from HTML, but here we
    # always want a string
    return html or ''

class MAPIMsgStoreFolder:
    def __init__(self, msgstore, id, name, count):
        self.msgstore = msgstore
        self.id = id
        self.name = name
        self.count = count

    def __repr__(self):
        return "<%s '%s' (%d items), id=%s/%s>" % (self.__class__.__name__,
                                                self.name,
                                                self.count,
                                                mapi.HexFromBin(self.id[0]),
                                                mapi.HexFromBin(self.id[1]))

    def __eq__(self, other):
        if other is None: return False
        ceid = self.msgstore.session.CompareEntryIDs
        return ceid(self.id[0], other.id[0]) and \
               ceid(self.id[1], other.id[1])

    def __ne__(self, other):
        return not self.__eq__(other)

    def GetID(self):
        return mapi.HexFromBin(self.id[0]), mapi.HexFromBin(self.id[1])

    def GetFQName(self):
        parts = []
        parent = self
        while parent is not None:
            parts.insert(0, parent.name)
            try:
                # Ignore errors fetching parents - the caller just wants the
                # name - it may not be correctly 'fully qualified', but at
                # least we get something.
                parent = parent.GetParent()
            except MsgStoreException:
                break
        # We now end up with [0] being an empty string??, [1] being the
        # information store root folder name, etc.  Outlook etc all just
        # use the information store name here.
        if parts and not parts[0]:
            del parts[0]
        # Don't catch exceptions on the item itself - that is fatal,
        # and should be caught by the caller.
        # Replace the "root" folder name with the information store name
        # as Outlook, our Folder selector etc do.
        mapi_store = self.msgstore._GetMessageStore(self.id[0])
        hr, data = mapi_store.GetProps((PR_DISPLAY_NAME_A,), 0)
        name = data[0][1]
        if parts:
            # and replace with new name
            parts[0] = name
        else:
            # This can happen for the very root folder (ie, parent of the
            # top-level folder shown by Outlook.  This folder should *never*
            # be used directly.
            parts = [name]
            print "WARNING: It appears you are using the top-level root of " \
                  "the information store as a folder.  You probably don't "\
                  "want to do that"
        return "/".join(parts)

    def _FolderFromMAPIFolder(self, mapifolder):
        # Finally get the display name.
        hr, data = mapifolder.GetProps((PR_ENTRYID, PR_DISPLAY_NAME_A,), 0)
        eid = self.id[0], data[0][1]
        name = data[1][1]
        count = mapifolder.GetContentsTable(0).GetRowCount(0)
        return MAPIMsgStoreFolder(self.msgstore, eid, name, count)

    def GetParent(self):
        # return a folder object with the parent, or None if there is no
        # parent (ie, a top-level folder).  Raises an exception if there is
        # an error fetching the parent (which implies something wrong with the
        # item itself, rather than this being top-level)
        try:
            folder = self.msgstore._OpenEntry(self.id)
            prop_ids = PR_PARENT_ENTRYID,
            hr, data = folder.GetProps(prop_ids,0)
            # Put parent ids together
            parent_eid = data[0][1]
            parent_id = self.id[0], parent_eid
            if hr != 0 or \
               self.msgstore.session.CompareEntryIDs(parent_eid, self.id[1]):
                # No parent EID, or EID same as ours.
                return None
            parent = self.msgstore._OpenEntry(parent_id)
            # Finally get the item itself
            return self._FolderFromMAPIFolder(parent)
        except pythoncom.com_error, details:
            raise MsgStoreExceptionFromCOMException(details)

    def OpenEntry(self, iid = None, flags = None):
        return self.msgstore._OpenEntry(self.id, iid, flags)

    def GetOutlookItem(self):
        try:
            hex_item_id = mapi.HexFromBin(self.id[1])
            hex_store_id = mapi.HexFromBin(self.id[0])
            return self.msgstore.outlook.Session.GetFolderFromID(hex_item_id, hex_store_id)
        except pythoncom.com_error, details:
            raise MsgStoreExceptionFromCOMException(details)

    def GetMessageGenerator(self, only_filter_candidates = True):
        folder = self.OpenEntry()
        table = folder.GetContentsTable(0)
        table.SetColumns(MAPIMsgStoreMsg.message_init_props, 0)
        if only_filter_candidates:
            # Limit ourselves to IPM.* objects - ie, messages.
            restriction = (mapi.RES_PROPERTY,   # a property restriction
                           (mapi.RELOP_GE,      # >=
                            PR_MESSAGE_CLASS_A,   # of the this prop
                            (PR_MESSAGE_CLASS_A, "IPM."))) # with this value
            table.Restrict(restriction, 0)
        while 1:
            # Getting 70 at a time was the random number that gave best
            # perf for me ;)
            rows = table.QueryRows(70, 0)
            if len(rows) == 0:
                break
            for row in rows:
                # Our restriction helped, but may not have filtered
                # every message we don't want to touch.
                # Note no exception will be raised below if the message is
                # moved under us, as we don't need to access any properties.
                msg = MAPIMsgStoreMsg(self.msgstore, row)
                if not only_filter_candidates or msg.IsFilterCandidate():
                    yield msg

    def GetNewUnscoredMessageGenerator(self, scoreFieldName):
        folder = self.msgstore._OpenEntry(self.id)
        table = folder.GetContentsTable(0)
        # Resolve the field name
        resolve_props = ( (mapi.PS_PUBLIC_STRINGS, scoreFieldName), )
        resolve_ids = folder.GetIDsFromNames(resolve_props, 0)
        field_id = PROP_TAG( PT_DOUBLE, PROP_ID(resolve_ids[0]))
        # Setup the properties we want to read.
        table.SetColumns(MAPIMsgStoreMsg.message_init_props, 0)
        # Set up the restriction
        # Need to check message-flags
        # (PR_CONTENT_UNREAD is optional, and somewhat unreliable
        # PR_MESSAGE_FLAGS & MSGFLAG_READ is the official way)
        prop_restriction = (mapi.RES_BITMASK,   # a bitmask restriction
                               (mapi.BMR_EQZ,      # when bit is clear
                                PR_MESSAGE_FLAGS,
                                MSGFLAG_READ))
        exist_restriction = mapi.RES_EXIST, (field_id,)
        not_exist_restriction = mapi.RES_NOT, (exist_restriction,)
        # A restriction for the message class
        class_restriction = (mapi.RES_PROPERTY,   # a property restriction
                             (mapi.RELOP_GE,      # >=
                              PR_MESSAGE_CLASS_A,   # of the this prop
                              (PR_MESSAGE_CLASS_A, "IPM."))) # with this value
        # Put the final restriction together
        restriction = (mapi.RES_AND, (prop_restriction,
                                      not_exist_restriction,
                                      class_restriction))
        table.Restrict(restriction, 0)
        while 1:
            rows = table.QueryRows(70, 0)
            if len(rows) == 0:
                break
            for row in rows:
                # Note no exception will be raised below if the message is
                # moved under us, as we don't need to access any properties.
                msg = MAPIMsgStoreMsg(self.msgstore, row)
                if msg.IsFilterCandidate():
                    yield msg

    def IsReceiveFolder(self, msg_class = "IPM.Note"):
        # Is this folder the nominated "receive folder" for its store?
        try:
            mapi_store = self.msgstore._GetMessageStore(self.id[0])
            eid, ret_class = mapi_store.GetReceiveFolder(msg_class, 0)
            return mapi_store.CompareEntryIDs(eid, self.id[1])
        except pythoncom.com_error:
            # Error getting the receive folder from the store (or maybe  our
            # store - but that would be insane!).  Either way, we can't be it!
            return False

    def CreateFolder(self, name, comments = None, type = None,
                     open_if_exists = False, flags = None):
        if type is None: type = mapi.FOLDER_GENERIC
        if flags is None: flags = 0
        if open_if_exists: flags |= mapi.OPEN_IF_EXISTS
        folder = self.OpenEntry()
        ret = folder.CreateFolder(type, name, comments, None, flags)
        return self._FolderFromMAPIFolder(ret)

    def DoesFolderHaveOutlookField(self, field_name):
        # Returns True if the specified folder has an *Outlook* field with
        # the given name, False if the folder does not have it, or None
        # if we can't tell, or there was an error, etc.
        # We have discovered that Outlook stores 'Fields' for a folder as a
        # PR_USERFIELDS field in the hidden, 'associated' message with
        # message class IPC.MS.REN.USERFIELDS.  This is a binary property
        # which is undocumented, but probably could be reverse-engineered
        # with a little effort (see 'dump_props --dump-folder-user-props' for
        # an example of the raw data.  For now, the simplest thing appears
        # to be to check for a \0 character, followed by the property name
        # as an ascii string.
        try:
            folder = self.msgstore._OpenEntry(self.id)
            table = folder.GetContentsTable(mapi.MAPI_ASSOCIATED)
            restriction = (mapi.RES_PROPERTY,
                          (mapi.RELOP_EQ,
                           PR_MESSAGE_CLASS_A,
                           (PR_MESSAGE_CLASS_A, 'IPC.MS.REN.USERFIELDS')))
            cols = (PR_USERFIELDS,)
            table.SetColumns(cols, 0)
            rows = mapi.HrQueryAllRows(table, cols, restriction, None, 0)
            if len(rows)>1:
                print "Eeek - only expecting one row from IPC.MS.REN.USERFIELDS"
                print "got", repr(rows)
                return None
            if len(rows)==0:
                # New folders with no userdefined fields do not have such a row,
                # but this is a clear indication it does not exist.
                return False
            row = rows[0]
            val = GetPotentiallyLargeStringProp(folder, cols[0], row[0])
        except pythoncom.com_error, details:
            raise MsgStoreExceptionFromCOMException(details)
        if type(val) != type(''):
            print "Value type incorrect - expected string, got", repr(val)
            return None
        return val.find("\0" + field_name) >= 0

    def DeleteMessages(self, message_things):
        # A *permanent* delete - MAPI has no concept of 'Deleted Items',
        # only Outlook does.  If you want a "soft" delete, you must locate
        # deleted item (via a special ID) and move it to there yourself
        # message_things may be ID tuples, or MAPIMsgStoreMsg instances.
        real_ids = []
        for thing in message_things:
            if isinstance(thing, MAPIMsgStoreMsg):
                real_ids.append( thing.id[1] )
                thing.mapi_object = thing.id = thing.folder_id = None
            else:
                real_ids.append(self.msgstore.NormalizeID(thing)[1])
        try:
            folder = self.msgstore._OpenEntry(self.id)
            # Nuke my MAPI reference, and set my ID to None
            rc = folder.DeleteMessages(real_ids, 0, None, 0)
        except pythoncom.com_error, details:
            raise MsgStoreExceptionFromCOMException(details)

    def CreateTemporaryMessage(self, msg_flags = None):
        # Create a message designed to be used temporarily.  It is your
        # responsibility to delete when you are done with it.
        # If msg_flags is not None, it should be an integer for the
        # PR_MESSAGE_FLAGS property.  Note that Outlook appears to refuse
        # to set user properties on a message marked as 'unsent', which
        # is the default.  Setting to, eg, 1 marks it as a "not unsent, read"
        # message, which works fine with user properties.
        try:
            folder = self.msgstore._OpenEntry(self.id)
            imsg = folder.CreateMessage(None, 0)
            if msg_flags is not None:
                props = (PR_MESSAGE_FLAGS,msg_flags),
                imsg.SetProps(props)
            imsg.SaveChanges(0)
            hr, data = imsg.GetProps((PR_ENTRYID, PR_STORE_ENTRYID), 0)
            eid = data[0][1]
            storeid = data[1][1]
            msg_id = mapi.HexFromBin(storeid), mapi.HexFromBin(eid)
        except pythoncom.com_error, details:
            raise MsgStoreExceptionFromCOMException(details)
        return self.msgstore.GetMessage(msg_id)

class MAPIMsgStoreMsg:
    # All the properties we must initialize a message with.
    # These include all the IDs we need, parent IDs, any properties needed
    # to determine if this is a "filterable" message, etc
    message_init_props = (PR_ENTRYID, PR_STORE_ENTRYID, PR_SEARCH_KEY,
                          PR_PARENT_ENTRYID, # folder ID
                          PR_MESSAGE_CLASS_A, # 'IPM.Note' etc
                          PR_RECEIVED_BY_ENTRYID, # who received it
                          PR_SUBJECT_A,
                          PR_TRANSPORT_MESSAGE_HEADERS_A,
                          )

    def __init__(self, msgstore, prop_row):
        self.msgstore = msgstore
        self.mapi_object = None

        # prop_row is a single mapi property row, with fields as above.
        # NOTE: We can't trust these properties for "large" values
        # (ie, strings, PT_BINARY, objects etc.), as they sometimes come
        # from the IMAPITable (which has a 255 limit on property values)
        # and sometimes from the object itself (which has no restriction).
        # This limitation is documented by MAPI.
        # Thus, we don't trust "PR_TRANSPORT_MESSAGE_HEADERS_A" more than
        # to ask "does the property exist?"
        tag, eid = prop_row[0] # ID
        tag, store_eid = prop_row[1]
        tag, searchkey = prop_row[2]
        tag, parent_eid = prop_row[3]
        tag, msgclass = prop_row[4]
        recby_tag, recby = prop_row[5]
        tag, subject = prop_row[6]
        headers_tag, headers = prop_row[7]

        self.id = store_eid, eid
        self.folder_id = store_eid, parent_eid
        self.msgclass = msgclass
        self.subject = subject
        has_headers = PROP_TYPE(headers_tag)==PT_STRING8
        # Search key is the only reliable thing after a move/copy operation
        # only problem is that it can potentially be changed - however, the
        # Outlook client provides no such (easy/obvious) way
        # (ie, someone would need to really want to change it <wink>)
        # Thus, searchkey is our long-lived message key.
        self.searchkey = searchkey
        # To check if a message has ever been received, we check the
        # PR_RECEIVED_BY_ENTRYID flag.  Tim wrote in an old comment that
        # An article on the web said the distinction can't be made with 100%
        # certainty, but that a good heuristic is to believe that a
        # msg has been received iff at least one of these properties
        # has a sensible value: RECEIVED_BY_EMAIL_ADDRESS, RECEIVED_BY_NAME,
        # RECEIVED_BY_ENTRYID PR_TRANSPORT_MESSAGE_HEADERS
        # But MarkH can't find it, and believes and tests that
        # PR_RECEIVED_BY_ENTRYID is all we need (but has since discovered a
        # couple of messages without any PR_RECEIVED_BY properties - but *with*
        # PR_TRANSPORT_MESSAGE_HEADERS - *sigh*)
        self.was_received = PROP_TYPE(recby_tag) == PT_BINARY or has_headers
        self.dirty = False

    def __repr__(self):
        if self.id is None:
            id_str = "(deleted/moved)"
        else:
            id_str = mapi.HexFromBin(self.id[0]), mapi.HexFromBin(self.id[1])
        return "<%s, '%s' id=%s>" % (self.__class__.__name__,
                                     self.GetSubject(),
                                     id_str)

    # as per search-key comments above, we also "enforce" this at the Python
    # level.  2 different messages, but one copied from the other, will
    # return "==".
    # Not being consistent could cause subtle bugs, especially in interactions
    # with various test tools.
    # Compare the GetID() results if you need to know different messages.
    def __hash__(self):
        return hash(self.searchkey)

    def __eq__(self, other):
        ceid = self.msgstore.session.CompareEntryIDs
        return ceid(self.searchkey, other.searchkey)

    def __ne__(self, other):
        return not self.__eq__(other)

    def GetID(self):
        return mapi.HexFromBin(self.id[0]), mapi.HexFromBin(self.id[1])

    def GetSubject(self):
        return self.subject

    def GetOutlookItem(self):
        hex_item_id = mapi.HexFromBin(self.id[1])
        hex_store_id = mapi.HexFromBin(self.id[0])
        return self.msgstore.outlook.Session.GetItemFromID(hex_item_id, hex_store_id)

    def IsFilterCandidate(self):
        # We don't attempt to filter:
        # * Non-mail items
        # * Messages that weren't actually received - this generally means user
        #   composed messages yet to be sent, or copies of "sent items".
        # It does *not* exclude messages that were user composed, but still
        # actually received by the user (ie, when you mail yourself)
        # GroupWise generates IPM.Anti-Virus.Report.45 (but I'm not sure how
        # it manages given it is an external server, and as far as I can tell,
        # this does not appear in the headers.
        if test_suite_running:
            # While the test suite is running, we *only* filter test msgs.
            return self.subject == "SpamBayes addin auto-generated test message"
        class_check = self.msgclass.lower()
        for check in "ipm.note", "ipm.anti-virus":
            if class_check.startswith(check):
                break
        else:
            # Not matching class - no good
            return False
        # Must match msg class to get here.
        return self.was_received

    def _GetPotentiallyLargeStringProp(self, prop_id, row):
        return GetPotentiallyLargeStringProp(self.mapi_object, prop_id, row)

    def _GetMessageText(self):
        parts = self._GetMessageTextParts()
        # parts is (headers, body, html), but could possibly grow
        return "\n".join(parts)

    def _GetMessageTextParts(self):
        # This is almost reliable :).  The only messages this now fails for
        # are for "forwarded" messages, where the forwards are actually
        # in an attachment.  Later.
        # Note we *dont* look in plain text attachments, which we arguably
        # should.
        from spambayes import mboxutils

        self._EnsureObject()
        prop_ids = (PR_BODY_A,
                    MYPR_BODY_HTML_A,
                    PR_TRANSPORT_MESSAGE_HEADERS_A)
        hr, data = self.mapi_object.GetProps(prop_ids,0)
        body = self._GetPotentiallyLargeStringProp(prop_ids[0], data[0])
        html = self._GetPotentiallyLargeStringProp(prop_ids[1], data[1])
        headers = self._GetPotentiallyLargeStringProp(prop_ids[2], data[2])
        # xxx - not sure what to do if we have both.
        if not html:
            html = GetHTMLFromRTFProperty(self.mapi_object)

        # Some Outlooks deliver a strange notion of headers, including
        # interior MIME armor.  To prevent later errors, try to get rid
        # of stuff now that can't possibly be parsed as "real" (SMTP)
        # headers.
        headers = mboxutils.extract_headers(headers)

        # Mail delivered internally via Exchange Server etc may not have
        # headers - fake some up.
        if not headers:
            headers = self._GetFakeHeaders ()
        # Mail delivered via the Exchange Internet Mail MTA may have
        # gibberish at the start of the headers - fix this.
        elif headers.startswith("Microsoft Mail"):
            headers = "X-MS-Mail-Gibberish: " + headers

        if not html and not body:
            # Only ever seen this for "multipart/signed" messages, so
            # without any better clues, just handle this.
            # Find all attachments with
            # PR_ATTACH_MIME_TAG_A=multipart/signed
            table = self.mapi_object.GetAttachmentTable(0)
            restriction = (mapi.RES_PROPERTY,   # a property restriction
                           (mapi.RELOP_EQ,      # check for equality
                            PR_ATTACH_MIME_TAG_A,   # of the given prop
                            (PR_ATTACH_MIME_TAG_A, "multipart/signed")))
            try:
                rows = mapi.HrQueryAllRows(table,
                                           (PR_ATTACH_NUM,), # columns to get
                                           restriction,    # only these rows
                                           None,    # any sort order is fine
                                           0)       # any # of results is fine
            except pythoncom.com_error:
                # For some reason there are no rows we can get
                rows = []
            if len(rows) == 0:
                pass # Nothing we can fetch :(
            else:
                if len(rows) > 1:
                    print "WARNING: Found %d rows with multipart/signed" \
                          "- using first only" % len(rows)
                row = rows[0]
                (attach_num_tag, attach_num), = row
                assert attach_num_tag != PT_ERROR, \
                       "Error fetching attach_num prop"
                # Open the attachment
                attach = self.mapi_object.OpenAttach(attach_num,
                                                   None,
                                                   mapi.MAPI_DEFERRED_ERRORS)
                prop_ids = (PR_ATTACH_DATA_BIN,)
                hr, data = attach.GetProps(prop_ids, 0)
                attach_body = GetPotentiallyLargeStringProp(attach, prop_ids[0], data[0])
                # What we seem to have here now is a *complete* multi-part
                # mime message - that Outlook must have re-constituted on
                # the fly immediately after pulling it apart! - not unlike
                # exactly what we are doing ourselves right here - putting
                # it into a message object, so we can extract the text, so
                # we can stick it back into another one.  Ahhhhh.
                import email
                msg = email.message_from_string(attach_body)
                assert msg.is_multipart(), "Should be multi-part: %r" % attach_body
                # reduce down all sub messages, collecting all text/ subtypes.
                # (we could make a distinction between text and html, but
                # it is all joined together by this method anyway.)
                def collect_text_parts(msg):
                    collected = ''
                    if msg.is_multipart():
                        for sub in msg.get_payload():
                            collected += collect_text_parts(sub)
                    else:
                        if msg.get_content_maintype()=='text':
                            collected += msg.get_payload()
                        else:
                            #print "skipping content type", msg.get_content_type()
                            pass
                    return collected
                body = collect_text_parts(msg)

        return headers, body, html

    def _GetFakeHeaders(self):
        # This is designed to fake up some SMTP headers for messages
        # on an exchange server that do not have such headers of their own
        prop_ids = PR_SUBJECT_A, PR_DISPLAY_NAME_A, PR_DISPLAY_TO_A, PR_DISPLAY_CC_A
        hr, data = self.mapi_object.GetProps(prop_ids,0)
        subject = self._GetPotentiallyLargeStringProp(prop_ids[0], data[0])
        sender = self._GetPotentiallyLargeStringProp(prop_ids[1], data[1])
        to = self._GetPotentiallyLargeStringProp(prop_ids[2], data[2])
        cc = self._GetPotentiallyLargeStringProp(prop_ids[3], data[3])
        headers = ["X-Exchange-Message: true"]
        if subject: headers.append("Subject: "+subject)
        if sender: headers.append("From: "+sender)
        if to: headers.append("To: "+to)
        if cc: headers.append("CC: "+cc)
        return "\n".join(headers) + "\n"

    def _EnsureObject(self):
        if self.mapi_object is None:
            try:
                help_test_suite("MAPIMsgStoreMsg._EnsureObject")
                self.mapi_object = self.msgstore._OpenEntry(self.id)
            except pythoncom.com_error, details:
                raise MsgStoreExceptionFromCOMException(details)

    def GetEmailPackageObject(self, strip_mime_headers=True):
        # Return an email.Message object.
        #
        # strip_mime_headers is a hack, and should be left True unless you're
        # trying to display all the headers for diagnostic purposes.  If we
        # figure out something better to do, it should go away entirely.
        #
        # Problem #1:  suppose a msg is multipart/alternative, with
        # text/plain and text/html sections.  The latter MIME decorations
        # are plain missing in what _GetMessageText() returns.  If we leave
        # the multipart/alternative in the headers anyway, the email
        # package's "lax parsing" won't complain about not finding any
        # sections, but since the type *is* multipart/alternative then
        # anyway, the tokenizer finds no text/* parts at all to tokenize.
        # As a result, only the headers get tokenized.  By stripping
        # Content-Type from the headers (if present), the email pkg
        # considers the body to be text/plain (the default), and so it
        # does get tokenized.
        #
        # Problem #2:  Outlook decodes quoted-printable and base64 on its
        # own, but leaves any Content-Transfer-Encoding line in the headers.
        # This can cause the email pkg to try to decode the text again,
        # with unpleasant (but rarely fatal) results.  If we strip that
        # header too, no problem -- although the fact that a msg was
        # encoded in base64 is usually a good spam clue, and we miss that.
        #
        # Short course:  we either have to synthesize non-insane MIME
        # structure, or eliminate all evidence of original MIME structure.
        # Since we don't have a way to the former, by default this function
        # does the latter.
        import email
        text = self._GetMessageText()
        try:
            try:
                msg = email.message_from_string(text)
            except email.Errors.BoundaryError:
                # In case this is the
                #    "No terminating boundary and no trailing empty line"
                # flavor of BoundaryError, we can supply a trailing empty
                # line to shut it up.  It's certainly ill-formed MIME, and
                # probably spam.  We don't care about the exact MIME
                # structure, just the words it contains, so no harm and
                # much good in trying to suppress this error.
                try:
                    msg = email.message_from_string(text + "\n\n")
                except email.Errors.BoundaryError:
                    msg = None
            except email.Errors.HeaderParseError:
                # This exception can come from parsing the header *or* the
                # body of a mime message.
                msg = None
            # But even this doesn't get *everything*.  We can still see:
            #  "multipart message with no defined boundary" or the
            # HeaderParseError above.  Time to get brutal - hack out
            # the Content-Type header, so we see it as plain text.
            if msg is None:
                butcher_pos = text.lower().find("\ncontent-type: ")
                if butcher_pos < 0:
                    # This error just just gunna get caught below anyway
                    raise RuntimeError(
                        "email package croaked with a MIME related error, but "
                        "there appears to be no 'Content-Type' header")
                # Put it back together, skipping the original "\n" but
                # leaving the header leaving "\nSpamBayes-Content-Type: "
                butchered = text[:butcher_pos] + "\nSpamBayes-" + \
                            text[butcher_pos+1:] + "\n\n"
                msg = email.message_from_string(butchered)
        except:
            print "FAILED to create email.message from: ", `text`
            raise

        if strip_mime_headers:
            if msg.has_key('content-type'):
                del msg['content-type']
            if msg.has_key('content-transfer-encoding'):
                del msg['content-transfer-encoding']

        return msg

    def SetField(self, prop, val):
        # Future optimization note - from GetIDsFromNames doco
        # Name-to-identifier mapping is represented by an object's
        # PR_MAPPING_SIGNATURE property. PR_MAPPING_SIGNATURE contains
        # a MAPIUID structure that indicates the service provider
        # responsible for the object. If the PR_MAPPING_SIGNATURE
        # property is the same for two objects, assume that these
        # objects use the same name-to-identifier mapping.
        # [MarkH: MAPIUID objects are supported and hashable]

        # XXX If the SpamProb (Hammie, whatever) property is passed in as an
        # XXX int, Outlook displays the field as all blanks, and sorting on
        # XXX it doesn't do anything, etc.  I don't know why.  Since I'm
        # XXX running Python 2.2.2, the _MapiTypeMap above confuses ints
        # XXX with bools, but the problem persists even if I comment out the
        # XXX PT_BOOLEAN entry from that dict.  Dumping in prints below show
        # XXX that type_tag is 3 then, and that matches the defn of PT_I4 in
        # XXX my system header files.
        # XXX Later:  This works after all, but the field shows up as all
        # XXX blanks unless I *first* modify the view (like Messages) in
        # XXX Outlook to define a custom Integer field of the same name.
        self._EnsureObject()
        try:
            if type(prop) != type(0):
                props = ( (mapi.PS_PUBLIC_STRINGS, prop), )
                propIds = self.mapi_object.GetIDsFromNames(props, mapi.MAPI_CREATE)
                type_tag = _MapiTypeMap.get(type(val))
                if type_tag is None:
                    raise ValueError, "Don't know what to do with '%r' ('%s')" % (
                                         val, type(val))
                prop = PROP_TAG(type_tag, PROP_ID(propIds[0]))
            help_test_suite("MAPIMsgStoreMsg.SetField")
            if val is None:
                # Delete the property
                self.mapi_object.DeleteProps((prop,))
            else:
                self.mapi_object.SetProps(((prop,val),))
            self.dirty = True
        except pythoncom.com_error, details:
            raise MsgStoreExceptionFromCOMException(details)

    def GetField(self, prop):
        # xxx - still raise_errors?
        self._EnsureObject()
        if type(prop) != type(0):
            props = ( (mapi.PS_PUBLIC_STRINGS, prop), )
            prop = self.mapi_object.GetIDsFromNames(props, 0)[0]
            if PROP_TYPE(prop) == PT_ERROR: # No such property
                return None
            prop = PROP_TAG( PT_UNSPECIFIED, PROP_ID(prop))
        try:
            hr, props = self.mapi_object.GetProps((prop,), 0)
            ((tag, val), ) = props
            if PROP_TYPE(tag) == PT_ERROR:
                if val == mapi.MAPI_E_NOT_ENOUGH_MEMORY:
                    # Too big for simple properties - get via a stream
                    return GetPropFromStream(self.mapi_object, prop)
                return None
            return val
        except pythoncom.com_error, details:
            raise MsgStoreExceptionFromCOMException(details)

    def GetReadState(self):
        val = self.GetField(PR_MESSAGE_FLAGS)
        return (val&MSGFLAG_READ) != 0

    def SetReadState(self, is_read):
        try:
            self._EnsureObject()
            # always try and clear any pending delivery reports of read/unread
            help_test_suite("MAPIMsgStoreMsg.SetReadState")
            if is_read:
                self.mapi_object.SetReadFlag(USE_DEFERRED_ERRORS|SUPPRESS_RECEIPT)
            else:
                self.mapi_object.SetReadFlag(USE_DEFERRED_ERRORS|CLEAR_READ_FLAG)
            if __debug__:
                if self.GetReadState() != is_read:
                    print "MAPI SetReadState appears to have failed to change the message state"
                    print "Requested set to %s but the MAPI field after was %r" % \
                          (is_read, self.GetField(PR_MESSAGE_FLAGS))
        except pythoncom.com_error, details:
            raise MsgStoreExceptionFromCOMException(details)

    def Save(self):
        assert self.dirty, "asking me to save a clean message!"
        # It seems that *not* specifying mapi.MAPI_DEFERRED_ERRORS solves a lot
        # problems!  So we don't!
        try:
            help_test_suite("MAPIMsgStoreMsg.Save")
            self.mapi_object.SaveChanges(mapi.KEEP_OPEN_READWRITE)
            self.dirty = False
        except pythoncom.com_error, details:
            raise MsgStoreExceptionFromCOMException(details)

    def _DoCopyMove(self, folder, isMove):
        assert not self.dirty, \
               "asking me to move a dirty message - later saves will fail!"
        try:
            dest_folder = self.msgstore._OpenEntry(folder.id)
            source_folder = self.msgstore._OpenEntry(self.folder_id)
            flags = 0
            if isMove: flags |= MESSAGE_MOVE
            eid = self.id[1]
            help_test_suite("MAPIMsgStoreMsg._DoCopyMove")
            source_folder.CopyMessages((eid,),
                                        None,
                                        dest_folder,
                                        0,
                                        None,
                                        flags)
            # At this stage, I think we have lost meaningful ID etc values
            # Set everything to None to make it clearer what is wrong should
            # this become an issue.  We would need to re-fetch the eid of
            # the item, and set the store_id to the dest folder.
            self.id = None
            self.folder_id = None
        except pythoncom.com_error, details:
            raise MsgStoreExceptionFromCOMException(details)

    def MoveTo(self, folder):
        self._DoCopyMove(folder, True)

    def CopyTo(self, folder):
        self._DoCopyMove(folder, False)

    # Functions to perform operations, but report the error (ONCE!) to the
    # user.  Any errors are re-raised so the caller can degrade gracefully if
    # necessary.
    # XXX - not too happy with these - they should go, and the caller should
    # handle (especially now that we work exclusively with exceptions from
    # this module.
    def MoveToReportingError(self, manager, folder):
        try:
            self.MoveTo(folder)
        except MsgStoreException, details:
            ReportMAPIError(manager, "Moving a message", details.mapi_exception)
    def CopyToReportingError(self, manager, folder):
        try:
            self.MoveTo(folder)
        except MsgStoreException, details:
            ReportMAPIError(manager, "Copying a message", details.mapi_exception)

    def GetFolder(self):
        # return a folder object with the parent, or None
        folder_id = (mapi.HexFromBin(self.folder_id[0]),
                     mapi.HexFromBin(self.folder_id[1]))
        return self.msgstore.GetFolder(folder_id)

    def RememberMessageCurrentFolder(self):
        self._EnsureObject()
        try:
            folder = self.GetFolder()
            props = ( (mapi.PS_PUBLIC_STRINGS, "SpamBayesOriginalFolderStoreID"),
                      (mapi.PS_PUBLIC_STRINGS, "SpamBayesOriginalFolderID")
                      )
            resolve_ids = self.mapi_object.GetIDsFromNames(props, mapi.MAPI_CREATE)
            prop_ids = PROP_TAG( PT_BINARY, PROP_ID(resolve_ids[0])), \
                       PROP_TAG( PT_BINARY, PROP_ID(resolve_ids[1]))

            prop_tuples = (prop_ids[0],folder.id[0]), (prop_ids[1],folder.id[1])
            self.mapi_object.SetProps(prop_tuples)
            self.dirty = True
        except pythoncom.com_error, details:
            raise MsgStoreExceptionFromCOMException(details)

    def GetRememberedFolder(self):
        props = ( (mapi.PS_PUBLIC_STRINGS, "SpamBayesOriginalFolderStoreID"),
                  (mapi.PS_PUBLIC_STRINGS, "SpamBayesOriginalFolderID")
                  )
        try:
            self._EnsureObject()
            resolve_ids = self.mapi_object.GetIDsFromNames(props, mapi.MAPI_CREATE)
            prop_ids = PROP_TAG( PT_BINARY, PROP_ID(resolve_ids[0])), \
                       PROP_TAG( PT_BINARY, PROP_ID(resolve_ids[1]))
            hr, data = self.mapi_object.GetProps(prop_ids,0)
            if hr != 0:
                return None
            (store_tag, store_id), (eid_tag, eid) = data
            folder_id = mapi.HexFromBin(store_id), mapi.HexFromBin(eid)
            help_test_suite("MAPIMsgStoreMsg.GetRememberedFolder")
            return self.msgstore.GetFolder(folder_id)
        except:
            print "Error locating origin of message", self
            return None

def test():
    from win32com.client import Dispatch
    outlook = Dispatch("Outlook.Application")
    inbox = outlook.Session.GetDefaultFolder(constants.olFolderInbox)
    folder_id = inbox.Parent.StoreID, inbox.EntryID
    store = MAPIMsgStore()
    for folder in store.GetFolderGenerator([folder_id,], True):
        print folder
        for msg in folder.GetMessageGenerator():
            print msg
    store.Close()

if __name__=='__main__':
    test()


syntax highlighted by Code2HTML, v. 0.9.1