ecederstrand/exchangelib

error in function to_xml in fields.AppointmentStateField

FirePanda169 opened this issue · 7 comments

Describe the bug
When trying to move a CalendarItem from one mailbox to another, an error occurs with the types of AppointmentStateField
It occurs when you try to create it in a new mailbox.
It occurs due to a type mismatch in the conversion.
AppointmentStateField.from_xml returns type tuple, but AppointmentStateField.value_cls = int

To Reproduce

Expected behavior

Log output

Additional context
Python 3.11.8
exchangelib 5.2.0

Can you please provide some example code showing what you tried to do? And the error message or stack trace that you got? CalendarItem.appointment_state is a read-only field, so it's surprising that you reached AppointmentStateField.from_xml.

I'm trying to transfer all messages from one mailbox to another.
Messages like MeetingRequest too.

Example
get_folder - GetFolder
get_item - GetItem
create_item - CreateItem
These are wrappers on functions for convenience.

...
message_source = ews_control_source.get_item(task.source_message_oid, task.source_message_changekey)
folder_parent_target = ews_control_target.get_folder(task.target_folder_oid)
...
message_target = ews_control_target.create_item(folder_parent_target, message_source)

task.target_message_oid = message_target.id
task.target_message_changekey = message_target.changekey

stack trace

Traceback (most recent call last):
  File "/app/t2t_migrator/workers/queue_base.py", line 70, in __call__
    await current_function(task)
  File "/app/t2t_migrator/workers/queue_mail_message.py", line 71, in ews_mail_message_create
    await self.mail_control.create_mail_message_by_ews(
  File "/app/t2t_migrator/controls/mail.py", line 1398, in create_mail_message_by_ews
    message_target = ews_control_target.create_item(folder_parent_target, message_source)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/t2t_migrator/controls/ews.py", line 101, in create_item
    return list(CreateItem(account=self.account).call(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/root/.cache/pypoetry/virtualenvs/migrationt2t-9TtSrW0h-py3.11/lib/python3.11/site-packages/exchangelib/services/common.py", line 225, in _elems_to_objs
    for elem in elems:
  File "/root/.cache/pypoetry/virtualenvs/migrationt2t-9TtSrW0h-py3.11/lib/python3.11/site-packages/exchangelib/services/common.py", line 287, in _chunked_get_elements
    yield from self._get_elements(payload=payload_func(chunk, **kwargs))
                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/root/.cache/pypoetry/virtualenvs/migrationt2t-9TtSrW0h-py3.11/lib/python3.11/site-packages/exchangelib/services/create_item.py", line 95, in get_payload
    set_xml_value(item_elems, item, version=self.account.version)
  File "/root/.cache/pypoetry/virtualenvs/migrationt2t-9TtSrW0h-py3.11/lib/python3.11/site-packages/exchangelib/util.py", line 259, in set_xml_value
    elem.append(value.to_xml(version=version))
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/root/.cache/pypoetry/virtualenvs/migrationt2t-9TtSrW0h-py3.11/lib/python3.11/site-packages/exchangelib/properties.py", line 319, in to_xml
    self.clean(version=version)
  File "/root/.cache/pypoetry/virtualenvs/migrationt2t-9TtSrW0h-py3.11/lib/python3.11/site-packages/exchangelib/properties.py", line 300, in clean
    setattr(self, f.name, f.clean(val, version=version))
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/root/.cache/pypoetry/virtualenvs/migrationt2t-9TtSrW0h-py3.11/lib/python3.11/site-packages/exchangelib/fields.py", line 444, in clean
    value = super().clean(value, version=version)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/root/.cache/pypoetry/virtualenvs/migrationt2t-9TtSrW0h-py3.11/lib/python3.11/site-packages/exchangelib/fields.py", line 334, in clean
    raise TypeError(f"Field {self.name!r} value {value!r} must be of type {self.value_cls}")
TypeError: Field 'appointment_state' value ('Meeting', 'Received') must be of type <class 'int'>

You didn't include enough code for me to be able to reproduce.

Anyway, this is not a bug. You're calling CreateItem directly in "/app/t2t_migrator/controls/ews.py", line 101. You're free to use the low-level components of exchangelib, but then you need to handle the details yourself, for example which fields are read-only and which aren't.

If you want exchangelib to handle all that, then do something like CalendarItem(account=target_account, subject=..., ...).save().

I still think this is a type error.
This happens because AppointmentStateField inherits from IntegerField and AppointmentStateField.value_cls = int, and AppointmentStateField.from_xml returns tuple[str].

I understand that this is a read-only field. It does not go further into the request. Even if it goes away (by copying the library locally and conducting experiments), it does not create an error.
For myself in the fork, I will leave the code AppointmentStateField.from_xml, which does not throw an exception.

def from_xml(self, elem, account):
   return super().from_xml(elem=elem, account=account)

Thank you.

I have one more question.
I use the same code to transfer all messages and there was a problem that all letters are marked Draft.
Using ExtendedProperty, I can remove this.

# add property
class IsDraft(ExtendedProperty):
    property_tag = 0x0E07  # 3591
    property_type = "Integer"
...
Item.register("extended_is_draft", IsDraft)
...

message_source = ews_control_source.get_item(task.source_message_oid, task.source_message_changekey)
message_source.extended_is_draft = message_source.is_draft
message_target = ews_control_target.create_item(folder_parent_target, message_source)

Can you tell me if there could be a similar property in ExtendedProperty for AppointmentState?

AppointmentStateField is special because it's implemented as a bitmask in EWS. EWS sends an integer in XML. Therefore, the field is implemented as an integer field. But bitmasks are annoying to work with in Python, so we help the user by expanding the bitmask options to a tuple. Since the field is read-only, there's no point in implementing AppointmentStateField.to_xml() because there's no path to it via the supported API in exchangelib.

It's also possible that AppointmentState is indeed read-write but we just haven't found the correct incantations to write back the value. But https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/appointmentstate explicitly states that the field is read-only.

I don't know if AppointmentState can be accessed via an extended property, unfortunately.

When transferring items from one account to another, you also have the possibility to export and upload.

I agree with you about working with bit masks and working with this field.
I have a specific task (data migration from one mailbox to another). We used imap, but we couldn’t find how to access archived mailboxes.

I'll try this option with Export and upload
Thank you