Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better support for calendar.mail.ru #403

Merged
merged 4 commits into from
Oct 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions caldav/lib/vcal.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ def fix(event):
)

if fixed2 != event:
## This obscure code will ensure efficient rate-limiting of the error
## logging. The "remove_bit" lambda will return 0 only for powers of
## two (2, 4, 8, 16, 32, 64, etc).
global fixup_error_loggings
fixup_error_loggings += 1
is_power_of_two = lambda n: not (n & (n - 1))
Expand Down
36 changes: 29 additions & 7 deletions caldav/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,7 @@ def calendar_user_address_set(self) -> List[Optional[str]]:
)

if _addresses is None:
raise ValueError("Unexpected value None for _addresses")
raise error.NotFoundError("No calendar user addresses given from server")

assert not [x for x in _addresses if x.tag != dav.Href().tag]
addresses = list(_addresses)
Expand Down Expand Up @@ -1216,9 +1216,30 @@ def search(
raise error.ConsistencyError(
"Inconsistent usage parameters: xml together with other search options"
)
(response, objects) = self._request_report_build_resultlist(
xml, comp_class, props=props
)
try:
(response, objects) = self._request_report_build_resultlist(
xml, comp_class, props=props
)
except error.ReportError as err:
## Hack for some calendar servers
## yielding 400 if the search does not include compclass.
## Partial fix for https://github.com/python-caldav/caldav/issues/401
## This assumes the client actually wants events and not tasks
## The calendar server in question did not support tasks
## However the most correct would probably be to join
## events, tasks and journals.
## TODO: we need server compatibility hints!
## https://github.com/python-caldav/caldav/issues/402
if not comp_class and not "400" in err.reason:
return self.search(
event=True,
include_completed=include_completed,
sort_keys=sort_keys,
split_expanded=split_expanded,
props=props,
**kwargs,
)
raise

for o in objects:
## This would not be needed if the servers would follow the standard ...
Expand Down Expand Up @@ -2408,12 +2429,13 @@ def change_attendee_status(self, attendee: Optional[Any] = None, **kwargs) -> No
if self.client is None:
raise ValueError("Unexpected value None for self.client")

attendee = self.client.principal()
attendee = self.client.principal_address or self.client.principal()

cnt = 0

if isinstance(attendee, Principal):
for addr in attendee.calendar_user_address_set():
attendee_emails = attendee.calendar_user_address_set()
for addr in attendee_emails:
try:
self.change_attendee_status(addr, **kwargs)
## TODO: can probably just return now
Expand All @@ -2431,7 +2453,7 @@ def change_attendee_status(self, attendee: Optional[Any] = None, **kwargs) -> No
attendee_lines = ical_obj["attendee"]
if isinstance(attendee_lines, str):
attendee_lines = [attendee_lines]
strip_mailto = lambda x: str(x).replace("mailto:", "").lower()
strip_mailto = lambda x: str(x).lower().replace("mailto:", "")
for attendee_line in attendee_lines:
if strip_mailto(attendee_line) == strip_mailto(attendee):
attendee_line.params.update(kwargs)
Expand Down
11 changes: 10 additions & 1 deletion tests/compatibility_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
'broken_expand':
"""Server-side expand seems to work, but delivers wrong data""",

'no_current-user-principal':
"""Current user principal not supported by the server (flag is ignored by the tests as for now - pass the principal URL as the testing URL and it will work, albeit with one warning""",

'no_recurring':
"""Server is having issues with recurring events and/or todos. """
"""date searches covering recurrances may yield no results, """
Expand All @@ -29,6 +32,9 @@
'no_scheduling':
"""RFC6833 is not supported""",

'no_scheduling_mailbox':
"""Parts of RFC6833 is supported, but not the existence of inbox/mailbox""",

'no_default_calendar':
"""The given user starts without an assigned default calendar """
"""(or without pre-defined calendars at all)""",
Expand Down Expand Up @@ -188,7 +194,10 @@
"""The is-not-defined in a calendar-query not working as it should - see https://gitlab.com/davical-project/davical/-/issues/281""",

'search_needs_comptype':
"""The server may not always come up with anything useful when searching for objects and omitting to specify weather one wants to see tasks or events""",
"""The server may not always come up with anything useful when searching for objects and omitting to specify weather one wants to see tasks or events. https://github.com/python-caldav/caldav/issues/401""",

'search_always_needs_comptype':
"""calendar.mail.ru: the server throws 400 when searching for objects and omitting to specify weather one wants to see tasks or events. `calendar.objects()` throws 404, even if there are events. https://github.com/python-caldav/caldav/issues/401""",

'robur_rrule_freq_yearly_expands_monthly':
"""Robur expands a yearly event into a monthly event. I believe I've reported this one upstream at some point, but can't find back to it""",
Expand Down
135 changes: 117 additions & 18 deletions tests/test_caldav.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,12 @@
"ctuid4",
"ctuid5",
"ctuid6",
"tsst1",
"tsst2",
"tsst3",
"tsst4",
"tsst5",
"tsst6",
"test1",
"test2",
"test3",
"test4",
"test5",
"test6",
)
## TODO: todo7 is an item without uid. Should be taken care of somehow.

Expand Down Expand Up @@ -338,6 +338,79 @@
END:VCALENDAR
"""

attendee1 = """
BEGIN:VCALENDAR
PRODID:-//Example Corp.//CalDAV Client//EN
VERSION:2.0
BEGIN:VEVENT
STATUS:CANCELLED
UID:test-attendee1
X-MICROSOFT-DISALLOW-COUNTER:true
DTSTART;TZID=Europe/Moscow:20240607T100000
DTEND;TZID=Europe/Moscow:20240607T103000
LAST-MODIFIED:20240610T063933Z
DTSTAMP:20240618T063824Z
CREATED:00010101T000000Z
SUMMARY:test
SEQUENCE:0
TRANSP:OPAQUE
X-MOZ-LASTACK:20240610T063933Z
ORGANIZER;CN=:mailto:[email protected]
ATTENDEE;PARTSTAT=ACCEPTED;RSVP=true;ROLE=REQ-PARTICIPANT:mailto:[email protected]
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=DECLINED:mailto:[email protected]
END:VEVENT
BEGIN:VTIMEZONE
TZID:Europe/Moscow
TZURL:http://tzurl.org/zoneinfo-outlook/Europe/Moscow
X-LIC-LOCATION:Europe/Moscow
BEGIN:STANDARD
TZNAME:MSK
TZOFFSETFROM:+0300
TZOFFSETTO:+0300
DTSTART:19700101T000000
END:STANDARD
END:VTIMEZONE
END:VCALENDAR
"""

attendee2 = """
BEGIN:VCALENDAR
PRODID:-//MailRu//MailRu Calendar API -//EN
VERSION:2.0
BEGIN:VEVENT
STATUS:CANCELLED
UID:EB424921-C4D3-46A6-B827-9A92A90D6788
X-MICROSOFT-DISALLOW-COUNTER:true
DTSTART;TZID=Europe/Moscow:20240607T100000
DTEND;TZID=Europe/Moscow:20240607T103000
LAST-MODIFIED:20240610T063933Z
DTSTAMP:20240618T064033Z
CREATED:00010101T000000Z
SUMMARY:test
SEQUENCE:0
TRANSP:OPAQUE
X-MOZ-LASTACK:20240610T063933Z
ORGANIZER;CN=:mailto:[email protected]
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=true:mailto:knazarov@i
-core.ru
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=DECLINED:mailto:[email protected]
u
END:VEVENT
BEGIN:VTIMEZONE
TZID:Europe/Moscow
TZURL:http://tzurl.org/zoneinfo-outlook/Europe/Moscow
X-LIC-LOCATION:Europe/Moscow
BEGIN:STANDARD
TZNAME:MSK
TZOFFSETFROM:+0300
TZOFFSETTO:+0300
DTSTART:19700101T000000
END:STANDARD
END:VTIMEZONE
END:VCALENDAR
"""


sched = sched_template % (
str(uuid.uuid4()),
"%2i%2i%2i" % (random.randint(0, 23), random.randint(0, 59), random.randint(0, 59)),
Expand Down Expand Up @@ -613,8 +686,8 @@ def _fixCalendar(self, **kwargs):
ret = self.principal.make_calendar(
name=name, cal_id=self.testcal_id, **kwargs
)
## TEMP - checking that the calendar works
ret.events()
if self.check_compatibility_flag("search_always_needs_comptype"):
ret.objects = lambda load_objects: ret.events()
if self.cleanup_regime == "post":
self.calendars_used.append(ret)
return ret
Expand All @@ -633,11 +706,15 @@ def testSupport(self):

def testSchedulingInfo(self):
self.skip_on_compatibility_flag("no_scheduling")
inbox = self.principal.schedule_inbox()
outbox = self.principal.schedule_outbox()
calendar_user_address_set = self.principal.calendar_user_address_set()
me_a_participant = self.principal.get_vcal_address()

def testSchedulingMailboxes(self):
self.skip_on_compatibility_flag("no_scheduling")
self.skip_on_compatibility_flag("no_scheduling_mailbox")
inbox = self.principal.schedule_inbox()
outbox = self.principal.schedule_outbox()

def testPropfind(self):
"""
Test of the propfind methods. (This is sort of redundant, since
Expand Down Expand Up @@ -792,6 +869,22 @@ def testCreateDeleteCalendar(self):
with pytest.raises(self._notFound()):
self.principal.calendar(name="Yep", cal_id=self.testcal_id).events()

def testChangeAttendeeStatusWithEmailGiven(self):
self.skip_on_compatibility_flag("read_only")
c = self._fixCalendar()
event = c.save_event(
uid="test1",
dtstart=datetime(2015, 10, 10, 8, 7, 6),
dtend=datetime(2015, 10, 10, 9, 7, 6),
ical_fragment="ATTENDEE;ROLE=OPT-PARTICIPANT;PARTSTAT=TENTATIVE:MAILTO:[email protected]",
)
event.change_attendee_status(
attendee="[email protected]", PARTSTAT="ACCEPTED"
)
event.save()
event = c.event_by_uid("test1")
## TODO: work in progress ... see https://github.com/python-caldav/caldav/issues/399

def testCreateEvent(self):
self.skip_on_compatibility_flag("read_only")
c = self._fixCalendar()
Expand Down Expand Up @@ -1212,7 +1305,10 @@ def testSearchEvent(self):
assert len(all_events) == 3

## Search with todo flag set should yield no events
no_events = c.search(todo=True)
try:
no_events = c.search(todo=True)
except:
no_events = []
assert len(no_events) == 0

## Date search should be possible
Expand Down Expand Up @@ -1343,39 +1439,39 @@ def testSearchSortTodo(self):
summary="1 task overdue",
due=date(2022, 12, 12),
dtstart=date(2022, 10, 11),
uid="tsst1",
uid="test1",
)
t2 = c.save_todo(
summary="2 task future",
due=datetime.now() + timedelta(hours=15),
dtstart=datetime.now() + timedelta(minutes=15),
uid="tsst2",
uid="test2",
)
t3 = c.save_todo(
summary="3 task future due",
due=datetime.now() + timedelta(hours=15),
dtstart=datetime(2022, 12, 11, 10, 9, 8),
uid="tsst3",
uid="test3",
)
t4 = c.save_todo(
summary="4 task priority is set to nine which is the lowest",
priority=9,
uid="tsst4",
uid="test4",
)
t5 = c.save_todo(
summary="5 task status is set to COMPLETED and this will disappear from the ordinary todo search",
status="COMPLETED",
uid="tsst5",
uid="test5",
)
t6 = c.save_todo(
summary="6 task has categories",
categories="home,garden,sunshine",
uid="tsst6",
uid="test6",
)

def check_order(tasks, order):
assert [str(x.icalendar_component["uid"]) for x in tasks] == [
"tsst" + str(x) for x in order
"test" + str(x) for x in order
]

all_tasks = c.search(todo=True, sort_keys=("uid",))
Expand Down Expand Up @@ -1570,6 +1666,7 @@ def testCreateChildParent(self):

def testSetDue(self):
self.skip_on_compatibility_flag("read_only")
self.skip_on_compatibility_flag("no_todo")

c = self._fixCalendar(supported_calendar_component_set=["VTODO"])

Expand Down Expand Up @@ -1990,6 +2087,7 @@ def testTodoCompletion(self):

def testTodoRecurringCompleteSafe(self):
self.skip_on_compatibility_flag("read_only")
self.skip_on_compatibility_flag("no_todo")
c = self._fixCalendar(supported_calendar_component_set=["VTODO"])
t6 = c.save_todo(todo6, status="NEEDS-ACTION")
if not self.check_compatibility_flag("rrule_takes_no_count"):
Expand Down Expand Up @@ -2017,6 +2115,7 @@ def testTodoRecurringCompleteSafe(self):

def testTodoRecurringCompleteThisandfuture(self):
self.skip_on_compatibility_flag("read_only")
self.skip_on_compatibility_flag("no_todo")
c = self._fixCalendar(supported_calendar_component_set=["VTODO"])
t6 = c.save_todo(todo6, status="NEEDS-ACTION")
if not self.check_compatibility_flag("rrule_takes_no_count"):
Expand Down