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