Skip to content
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
5 changes: 5 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ jobs:
# Create test user
docker exec -e OC_PASS="testpass" ${{ job.services.nextcloud.id }} php occ user:add --password-from-env --display-name="Test User" testuser || echo "User may already exist"

# Create scheduling test users (user1-user3)
for i in 1 2 3; do
docker exec -e OC_PASS="testpass${i}" ${{ job.services.nextcloud.id }} php occ user:add --password-from-env --display-name="User ${i}" "user${i}" || echo "user${i} may already exist"
done

# Enable calendar and contacts apps
docker exec ${{ job.services.nextcloud.id }} php occ app:enable calendar || true
docker exec ${{ job.services.nextcloud.id }} php occ app:enable contacts || true
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ repos:
rev: lychee-v0.22.0
hooks:
- id: lychee
args: ["--no-progress", "--timeout", "10", "--exclude-path", ".lycheeignore"]
args: ["--no-progress", "--timeout", "10", "--exclude-path", ".lycheeignore", "--max-cache-age=30d", "--cache"]
stages: [pre-push]
exclude: ^tests/test_caldav_unit\.py$
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ This project should adhere to [Semantic Versioning](https://semver.org/spec/v2.0
* `Calendar.get_supported_components()`
* raised `KeyError` when the server did not include the `supported-calendar-component-set` property in its response. RFC 4791 section 5.2.3 states this property is optional and that its absence means all component types are accepted; the method now returns the RFC default `["VEVENT", "VTODO", "VJOURNAL"]` in that case, trimmed by any known server limitations from the compatibility hints (e.g. if `save-load.todo` is `unsupported`, `VTODO` is excluded). Fixes https://github.com/python-caldav/caldav/issues/653
* async path returned an unawaited coroutine instead of the actual result.
* `accept_invite()` (and `decline_invite()`, `tentatively_accept_invite()`) now fall back to the client username as the attendee email address when the server does not expose the `calendar-user-address-set` property (RFC6638 §2.4.1). A `NotFoundError` with a descriptive message is raised when the username is also not an email address. Fixes https://github.com/python-caldav/caldav/issues/399

## [3.1.0] - 2026-03-19

Expand Down
61 changes: 50 additions & 11 deletions caldav/calendarobjectresource.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,18 +168,42 @@ def set_end(self, end, move_dtstart=False):

i.add(self._ENDPARAM, end)

def add_organizer(self) -> None:
def add_organizer(self, organizer=None) -> None:
"""
goes via self.client, finds the principal, figures out the right attendee-format and adds an
organizer line to the event
Add (or replace) the ORGANIZER field on the calendar component.

If *organizer* is omitted the current principal is used (requires
``self.client`` to be set). The *organizer* argument accepts the
same types as :meth:`add_attendee`:

* A :class:`~caldav.Principal` object
* A :class:`icalendar.vCalAddress` object
* A ``"mailto:user@example.com"`` string
* A plain email address string (``"mailto:"`` is prepended automatically)

Any pre-existing ORGANIZER field is removed before the new one is added.
"""
if self.client is None:
raise ValueError("Unexpected value None for self.client")
from .collection import Principal as _Principal ## avoid circular import

principal = self.client.principal()
## TODO: remove Organizer-field, if exists
## TODO: what if walk returns more than one vevent?
self.icalendar_component.add("organizer", principal.get_vcal_address())
if organizer is None:
if self.client is None:
raise ValueError("Unexpected value None for self.client")
organizer_obj = self.client.principal().get_vcal_address()
elif isinstance(organizer, _Principal):
organizer_obj = organizer.get_vcal_address()
elif isinstance(organizer, vCalAddress):
organizer_obj = organizer
elif isinstance(organizer, str):
if organizer.startswith("mailto:"):
organizer_obj = vCalAddress(organizer)
else:
organizer_obj = vCalAddress("mailto:" + organizer)
else:
raise ValueError(f"Unsupported organizer type: {type(organizer)!r}")

ievent = self.icalendar_component
ievent.pop("organizer", None)
ievent.add("organizer", organizer_obj)

def split_expanded(self) -> list[Self]:
"""This was used internally for processing search results.
Expand Down Expand Up @@ -696,7 +720,7 @@ def is_invite_request(self) -> bool:
def is_invite_reply(self) -> bool:
"""
Returns True if the object is a reply, see
:rfc:`2446#section-3.2.3`.
:rfc:`5546#section-3.2`.
"""
self.load(only_if_unloaded=True)
return self.icalendar_instance.get("method", None) == "REPLY"
Expand Down Expand Up @@ -1031,7 +1055,22 @@ def change_attendee_status(self, attendee: Any | None = None, **kwargs) -> None:
cnt = 0

if isinstance(attendee, Principal):
attendee_emails = attendee.calendar_user_address_set()
try:
attendee_emails = attendee.calendar_user_address_set()
except error.NotFoundError:
## Server does not expose calendar-user-address-set (RFC6638 §2.4.1).
## Fall back to client.username if it looks like an email address.
## See https://github.com/python-caldav/caldav/issues/399
username = getattr(self.client, "username", None)
if username and "@" in str(username):
attendee_emails = ["mailto:" + username]
else:
raise error.NotFoundError(
"Server does not provide the calendar-user-address-set property "
"(RFC6638 §2.4.1) and the client username is not an email address. "
"Cannot determine which attendee to update. "
"Pass the attendee email address explicitly to change_attendee_status()."
) from None
for addr in attendee_emails:
try:
self.change_attendee_status(addr, **kwargs)
Expand Down
Loading
Loading