ecederstrand/exchangelib

Getting an error trying to retrieve all contacts from a mail file

champlin2 opened this issue · 9 comments

Have a test that runs through pytest that checks that a contact list in an EWS mail file contains the correct users. Was working with exchangelib 4.9.0 but since going to 5.0.2 its failing. I tried setting up debug logging (per https://ecederstrand.github.io/exchangelib/#troubleshooting) but didn't see any additional output.

>       for item in <user>.contacts.all():

test_4_4_x_features.py:79:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
C:\Python311\Lib\site-packages\exchangelib\queryset.py:270: in __iter__
    yield from self._format_items(items=self._query(), return_format=self.return_format)
C:\Python311\Lib\site-packages\exchangelib\queryset.py:345: in _item_yielder
    for i in iterable:
C:\Python311\Lib\site-packages\exchangelib\account.py:705: in fetch
    yield from self._consume_item_service(
C:\Python311\Lib\site-packages\exchangelib\account.py:400: in _consume_item_service
    yield from service_cls(account=self, chunk_size=chunk_size).call(**kwargs)
C:\Python311\Lib\site-packages\exchangelib\services\common.py:225: in _elems_to_objs
    for elem in elems:
C:\Python311\Lib\site-packages\exchangelib\services\common.py:287: in _chunked_get_elements
    yield from self._get_elements(payload=payload_func(chunk, **kwargs))
C:\Python311\Lib\site-packages\exchangelib\services\common.py:308: in _get_elements
    yield from self._response_generator(payload=payload)
C:\Python311\Lib\site-packages\exchangelib\services\common.py:684: in _get_elements_in_response
    container_or_exc = self._get_element_container(message=msg, name=self.element_container_name)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <exchangelib.services.get_item.GetItem object at 0x0000029CD5D57590>
message = <Element {http://schemas.microsoft.com/exchange/services/2006/messages}GetItemResponseMessage at 0x29cd5c50d80>
name = '{http://schemas.microsoft.com/exchange/services/2006/messages}Items'

    def _get_element_container(self, message, name=None):
        """Return the XML element in a response element that contains the elements we want the service to return. For
        example, in a GetFolder response, 'message' is the GetFolderResponseMessage element, and we return the 'Folders'
        element:

        <m:GetFolderResponseMessage ResponseClass="Success">
          <m:ResponseCode>NoError</m:ResponseCode>
          <m:Folders>
            <t:Folder>
              <t:FolderId Id="AQApA=" ChangeKey="AQAAAB" />
              [...]
            </t:Folder>
          </m:Folders>
        </m:GetFolderResponseMessage>

        Some service responses don't have a containing element for the returned elements ('name' is None). In
        that case, we return the 'SomeServiceResponseMessage' element.

        If the response contains a warning or an error message, we raise the relevant exception, unless the error class
        is contained in WARNINGS_TO_CATCH_IN_RESPONSE or ERRORS_TO_CATCH_IN_RESPONSE, in which case we return the
        exception instance.
        """
        # ResponseClass is an XML attribute of various SomeServiceResponseMessage elements: Possible values are:
        # Success, Warning, Error. See e.g.
        # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/finditemresponsemessage
        response_class = message.get("ResponseClass")
        # ResponseCode, MessageText: See
        # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/responsecode
        response_code = get_xml_attr(message, f"{{{MNS}}}ResponseCode")
        if response_class == "Success" and response_code == "NoError":
            if not name:
                return message
            container = message.find(name)
            if container is None:
                raise MalformedResponseError(f"No {name} elements in ResponseMessage ({xml_to_str(message)})")
            return container
        if response_code == "NoError":
            return True
        # Raise any non-acceptable errors in the container, or return the container or the acceptable exception instance
        msg_text = get_xml_attr(message, f"{{{MNS}}}MessageText")
        msg_xml = message.find(f"{{{MNS}}}MessageXml")
        if response_class == "Warning":
            try:
                raise self._get_exception(code=response_code, text=msg_text, msg_xml=msg_xml)
            except self.WARNINGS_TO_CATCH_IN_RESPONSE as e:
                return e
            except self.WARNINGS_TO_IGNORE_IN_RESPONSE as e:
                log.warning(str(e))
                container = message.find(name)
                if container is None:
                    raise MalformedResponseError(f"No {name} elements in ResponseMessage ({xml_to_str(message)})")
                return container
        # response_class == 'Error', or 'Success' and not 'NoError'
        try:
>           raise self._get_exception(code=response_code, text=msg_text, msg_xml=msg_xml)
E           exchangelib.errors.ErrorInternalServerError: An internal server error occurred. The operation failed., [0x004f0102] MapiReplyToBlob

C:\Python311\Lib\site-packages\exchangelib\services\common.py:606: ErrorInternalServerError

Thanks for the report! I don't think this has anything to do with the exchangelib version. I started getting this error in the test suite as well, long after releasing 5.0.2, and without changing any code. It happens for contacts and tasks.

I think this is an internal error in Exchange. I would suggest reaching out to your Exchange admins, or to Microsoft support, if this issue persists. Google doesn't return anything relevant matching the [0x004f0102] MapiReplyToBlob error message from the server.

Ok well that makes me feel better that someone else is seeing it too. Are you seeing it when getting contacts as well? How can I dump out the request/response within pytest so I can see what is actually being returned if MS support (the mail file is in onmicrosoft.com) asks?

Yes, I see it for contacts as well. When you enable debug logging, exchangelib will print out the exact XML request document causing the error and the XML response document containing the error message.

I have this at the top of the .py file where these tests reside but nothing being displayed in the output for the failed test:

import logging
from exchangelib.util import PrettyXmlHandler
.
.
.
avance = Account(
    primary_smtp_address=oauthparams.avance_smtp,
    config = configav,
    access_type=IMPERSONATION
)

logging.basicConfig(level=logging.DEBUG, handlers=[PrettyXmlHandler()])

I did a little bit of testing, and for contacts I only get this error if I ask for the unique_body field. Here's a quick test:

for f in sorted(account.contacts.allowed_item_fields(account.version), key=lambda i: i.name):
    if f.name in ("unique_body",):
        continue
    print(f.name, f.field_uri)
    list(self.account.contacts.all().only(f.name))

Ok, I only need to know that its a DistributionList and it's name (see code below). Is there a way to restructure that to avoid the error?

for item in <user>.contacts.all():
            if isinstance(item, DistributionList) and item.display_name == '<name>':
                if len(item.members) == 2:
                    for member in item.members:
                        if member.mailbox.name != 'user1@company.com' and member.mailbox.name != 'local.user@company.com':
                            errors.append(f'Unknown member {member} found in group')
                else:
                   errors.append(f'Group should contain 2 members but found {Len(item.members)}')

You can request the exact fields you need with the .only() modifier:

for item in <user>.contacts.all().only("display_name", "members"):
    ...

That should work around the issue.

Thanks! That fixed it

Should be fixed by 3685ff8 and f1f3dbe so account.contacts.all() also works without the .only().