Skip to content

Commit

Permalink
fix: Give GetUserAvailability enough info to parse datetimes as tz-aw…
Browse files Browse the repository at this point in the history
…are. Only warn about naive datetimes when they are unexpected. Fixes #1319
  • Loading branch information
Erik Cederstrand committed Jul 11, 2024
1 parent 7350d3d commit 9462ad2
Show file tree
Hide file tree
Showing 4 changed files with 27 additions and 6 deletions.
13 changes: 10 additions & 3 deletions exchangelib/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,9 +598,10 @@ class DateTimeBackedDateField(DateField):
def __init__(self, *args, **kwargs):
# Not all fields assume a default time of 00:00, so make this configurable
self._default_time = kwargs.pop("default_time", datetime.time(0, 0))
super().__init__(*args, **kwargs)
# Create internal field to handle datetime-only logic
self._datetime_field = DateTimeField(*args, **kwargs)
kwargs.pop("allow_naive", None)
super().__init__(*args, **kwargs)

def date_to_datetime(self, value):
return self._datetime_field.value_cls.combine(value, self._default_time).replace(tzinfo=UTC)
Expand Down Expand Up @@ -665,6 +666,10 @@ class DateTimeField(FieldURIField):

value_cls = EWSDateTime

def __init__(self, *args, **kwargs):
self.allow_naive = kwargs.pop("allow_naive", False)
super().__init__(*args, **kwargs)

def clean(self, value, version=None):
if isinstance(value, datetime.datetime):
if not value.tzinfo:
Expand All @@ -686,8 +691,10 @@ def from_xml(self, elem, account):
tz = account.default_timezone
log.info("Found naive datetime %s on field %s. Assuming timezone %s", e.local_dt, self.name, tz)
return e.local_dt.replace(tzinfo=tz)
# There's nothing we can do but return the naive date. It's better than assuming e.g. UTC.
log.warning("Returning naive datetime %s on field %s", e.local_dt, self.name)
if not self.allow_naive:
# There's nothing we can do but return the naive date. It's better than assuming e.g. UTC.
# Making this a hard error is probably too risky. Warn instead.
log.warning("Returning naive datetime %s on field %s", e.local_dt, self.name)
return e.local_dt
log.info("Cannot convert value '%s' on field '%s' to type %s", val, self.name, self.value_cls)
return None
Expand Down
3 changes: 2 additions & 1 deletion exchangelib/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -1857,7 +1857,8 @@ class AbsoluteDateTransition(BaseTransition):

ELEMENT_NAME = "AbsoluteDateTransition"

date = DateTimeBackedDateField(field_uri="DateTime")
# Values are returned as naive, and we have no timezone to hook up the values to yet
date = DateTimeBackedDateField(field_uri="DateTime", allow_naive=True)


class RecurringDayTransition(BaseTransition):
Expand Down
1 change: 1 addition & 0 deletions exchangelib/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ def get_free_busy_info(self, accounts, start, end, merged_free_busy_interval=30,

tz_definition = list(self.get_timezones(timezones=[start.tzinfo], return_full_timezone_data=True))[0]
return GetUserAvailability(self).call(
tzinfo=start.tzinfo,
mailbox_data=[
MailboxData(
email=account.primary_smtp_address if isinstance(account, Account) else account,
Expand Down
16 changes: 14 additions & 2 deletions exchangelib/services/get_user_availability.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from collections import namedtuple

from ..properties import FreeBusyView
from ..util import MNS, create_element, set_xml_value
from .common import EWSService
Expand All @@ -11,9 +13,14 @@ class GetUserAvailability(EWSService):

SERVICE_NAME = "GetUserAvailability"

def call(self, mailbox_data, timezone, free_busy_view_options):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.tzinfo = None

def call(self, tzinfo, mailbox_data, timezone, free_busy_view_options):
# TODO: Also supports SuggestionsViewOptions, see
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/suggestionsviewoptions
self.tzinfo = tzinfo
return self._elems_to_objs(
self._chunked_get_elements(
self.get_payload,
Expand All @@ -23,8 +30,13 @@ def call(self, mailbox_data, timezone, free_busy_view_options):
)
)

@property
def _timezone(self):
return self.tzinfo

def _elem_to_obj(self, elem):
return FreeBusyView.from_xml(elem=elem, account=None)
fake_account = namedtuple("Account", ["default_timezone"])(default_timezone=self.tzinfo)
return FreeBusyView.from_xml(elem=elem, account=fake_account)

def get_payload(self, mailbox_data, timezone, free_busy_view_options):
payload = create_element(f"m:{self.SERVICE_NAME}Request")
Expand Down

0 comments on commit 9462ad2

Please sign in to comment.