-1

I am trying using exchangelib to read emails with certain conversation_id and only read 2 attributes: eid and datetime_received. I don't know why the following code doesn't work:

from exchangelib import Credentials, Account, Configuration, DELEGATE, EWSDateTime, EWSTimeZone, ExtendedProperty, Message             
from exchangelib.properties import ConversationId
def read_latest_eid(conversation_id_, email_address_, password):
    convert_ID_to_entryID = lambda x: ''.join('{:02x}'.format(i) for i in x).upper().strip()
    class EntryID(ExtendedProperty):
        property_tag = 4095
        property_type = 'Binary'   
    try:
        Message.register('eid', EntryID)
    except:
        Message.deregister('eid')
        Message.register('eid', EntryID)
    
    credentials = Credentials(email_address_, password)
    config = Configuration(server='outlook.office365.com', credentials=credentials)
    account = Account("xxx@abc.eu", config=config, access_type=DELEGATE, 
                      autodiscover=False)
    result = []
    conversationid_to_search = ConversationId(conversation_id_)
    print('*** Search following conversation id:', conversationid_to_search, type(conversationid_to_search))
    print('*** id:', conversationid_to_search.id, type(conversationid_to_search.id))
    for i in account.inbox.filter(conversation_id=conversationid_to_search).only("datetime_received", "eid"):
        print('*** i:', i, type(i))
        result.append(list((i.datetime_received.astimezone().strftime("%d/%m/%Y, %H:%M:%S"),
                                convert_ID_to_entryID(i.eid))))
    Message.deregister('eid')
    return result

# For testing read_latest_id
if __name__ == "__main__":
    conversation_id = '7FD7341602EE405193F1F996D7DD8D6A'
    print(read_latest_eid(conversation_id))

Currently the error is:

*** Search following conversation id: ConversationId(id='7FD7341602EE405193F1F996D7DD8D6A') <class 'exchangelib.properties.ConversationId'>
*** id: 7FD7341602EE405193F1F996D7DD8D6A <class 'str'>
*** i: Id is malformed. <class 'exchangelib.errors.ErrorInvalidIdMalformed'>
Traceback (most recent call last):
  File "h:\Codes\Subscribe_Email\Read_Open_Mail.py", line 171, in <module>
    print(read_latest_eid(conversation_id))
  File "h:\Codes\Subscribe_Email\Read_Open_Mail.py", line 149, in read_latest_eid
    result.append(list((i.datetime_received.astimezone().strftime("%d/%m/%Y, %H:%M:%S"),
AttributeError: 'ErrorInvalidIdMalformed' object has no attribute 'datetime_received'

Any pointer how to solve it would be much appreciated.

Updated: I got the conversation_id from the locally running outlook programm. The following code outputs the currently selected email from my outlook. Refer to "return" line below.

def read_selected_email(outlook):
    # This function reads the properties of selected Outlook-Email.
   
    try:
        messages = outlook.ActiveExplorer().Selection
        message = messages(1)
        # print('Sender:', message.Sender)
        # print('Date:', message.ReceivedTime)
        # print('Titel:', message.subject)
        # print('Sender:', message.Sender)
        # print('Recipient:', message.To)
        # print('ConversationID', message.ConversationID)
        # received_time = str(datetime.strptime(str(message.ReceivedTime).rstrip(
        #     "+00:00"), '%Y-%m-%d %H:%M:%S').date())
        received_time = str(message.ReceivedTime).rstrip("+00:00")
        print(received_time)
        try:
            received_time = datetime.strptime(received_time, '%Y-%m-%d %H:%M:%S')
            # print('+1')
        except:
            try:
                received_time = datetime.strptime(received_time, '%Y-%m-%d %H:%M')
                # print('+2')
            except:
                received_time = datetime.strptime(received_time, '%Y-%m-%d %H:%M:%S.%f')
        received_time = str(received_time.date())
        # print('***', message)
        
        return [{'Datum': received_time, 'Titel': message.subject,
                'Sender': message.SenderName.split("(")[0], 'Recipient': 
                    message.To, 'Kommentar': '', 
                    'ConversationID': message.ConversationID}]       
    except AttributeError:
        print('*** No Email selected')
        pass
    except Exception as e:
        print(e)
        pass

# For testing read_selected_email
if __name__ == "__main__":
    outlook = win32com.client.Dispatch("Outlook.Application")
    outlook_mapi = outlook.GetNamespace("MAPI")
    print(read_selected_email(outlook))

Actually, I just want to reproduce the solution in https://github.com/ecederstrand/exchangelib/issues/261 by filtering the search based on conversation_id I am not sure, why it doesn't work on me.

gunardilin
  • 349
  • 3
  • 12

1 Answers1

1

Your exchange server does not like the format of your conversation ID. You're getting an ErrorInvalidIdMalformed error from the server.

Did you get the conversation ID directly from the server? If not, you may be able to convert the ID to the correct format using the ConvertID service, available via the account.protocol.convert_ids() method.

If you don't know the original format of the conversation ID, you can just try with all formats:

from exchangelib.properties import ID_FORMATS, EWS_ID, AlternateId

i = '7FD7341602EE405193F1F996D7DD8D6A'
for fmt in ID_FORMATS:
    res = list(account.protocol.convert_ids([
        AlternateId(
            id=i, format=fmt,
            mailbox=account.primary_smtp_address)
    ], destination_format=EWS_ID))[0]
    if isinstance(res, Exception):
        print(f'Error converting from {fmt} to {EWS_ID}: {res}')
    else:
        print(f'Sucessfully converted from {fmt} to {EWS_ID}: {res}')

It's also possible that you need to search using the MAPI property for the ConversationID field. Something like this should do (based on https://social.msdn.microsoft.com/Forums/office/en-US/5551df43-d833-4352-b27a-70e18ef71576/how-can-i-compare-conversation-id-from-exchange-web-services-with-outlook-conversation-id-?forum=outlookdev and https://github.com/ecederstrand/exchangelib/issues/146):

class MAPIConverstationID(ExtendedProperty):
    property_tag = 0x3013
    property_type = 'Binary'

Message.register('mapi_cid', MAPIConverstationID)

for m in account.filter(mapi_cid=my_win32com_cid):
    print(m.mapi_cid)
Erik Cederstrand
  • 9,643
  • 8
  • 39
  • 63
  • Hey Erik, thank you for your rush reply. I have updated my above post, to answer your question. The conversation_id should be correct, since I could with the same conversation_id find an email with library win32com.client. I don't like my current method with win32com, since it is brute force method (reading all emails in the last x days and comparing current conversation_id with certain saved conversation_id.) I am trying out to find an email directly based on conversation_id without the need to read all emails in last x days. Is it possible to do that with exchangelib? – gunardilin May 12 '21 at 11:36
  • By the way with function read_latest_eid, I would like to read all EntryID that belong to the same conversation_id. Afterwards, the function should output the newest EntryID (by comparing received_time). The code for that is not yet finished, as can be seen. With the newest EntryID, I can open the email directly using win32com. – gunardilin May 12 '21 at 11:54
  • 1
    If you didn’t get the ID from EWS then it could still be in the wrong format. Try getting a random conversation ID from exchangelib and compare the format. That should give you an idea about the format EWS expects. – Erik Cederstrand May 12 '21 at 15:37
  • Hello Erik, I have tried your last suggestion. By using win32com.client, an email gets folowing "message.ConversationID": "9926E4E8B126444C8A3D8F5EED66FEDB". By using exchangelib the same email get following "conversation_id": "AAQkADhkNzNkMDAxLTA2NTAtNDExMi05ZDMyLTU2OTIyOTFiMmExNQAQAJkm5OixJkRMij2PXu1m/ts=". Like you said, it seems that the wrong format is the reason why my above code didn't work. Can you tell me what format is how to convert to second format? I don't know how to, because both of them are shown as string and therefore I don't know what format it is.Thank you in advance. – gunardilin May 17 '21 at 06:25
  • 1
    You can brute force this in search for the right format. I've added some example code to do this in the answer above. – Erik Cederstrand May 18 '21 at 12:12
  • Thank you, I will try it as soon as possible. – gunardilin May 19 '21 at 13:03
  • 1
    With `binascii.hexlify(base64.b64decode('AAQkADhkNzNkMDAxLTA2NTAtNDExMi05ZDMyLTU2OTIyOTFiMmExNQAQAJkm5OixJkRMij2PXu1m/ts='))[86:]` I get the win32com version of the ID. I'm sure there are more official docs somewhere describing how to translate between the two formats, including the magic padding data. – Erik Cederstrand May 19 '21 at 18:13
  • Hello Erik, thank you very much for your assistance. You have helped me a lot, not only this time but also in the past (either directly or your past answers). Appreciate it. My code works now without using bruteforce method. – gunardilin May 20 '21 at 09:48
  • Hey Erik, hhmmm, my code is not working. I still need to find the docs to translate the formats because of the padding... – gunardilin May 20 '21 at 12:35
  • 1
    According to https://social.msdn.microsoft.com/Forums/office/en-US/5551df43-d833-4352-b27a-70e18ef71576/how-can-i-compare-conversation-id-from-exchange-web-services-with-outlook-conversation-id-?forum=outlookdev you may be able to search using the Outlook-formatted ConversationID by using an extended property. I've updated my answer. – Erik Cederstrand May 20 '21 at 17:39