- Check if hook is enabled before constructing hook notification items

- Skip/early exit email hook if notification item is not a VCALENDAR
This commit is contained in:
Nate Harris
2026-03-30 01:55:03 -06:00
parent 89edf01757
commit 3b9df7b87e
7 changed files with 123 additions and 61 deletions

View File

@@ -89,34 +89,38 @@ class ApplicationPartDelete(ApplicationBase):
if not access.check("D", item): if not access.check("D", item):
logger.info("delete of collection is prevented by config/option [rights] permit_delete_collection and not explicit allowed by permission 'D': %s", path) logger.info("delete of collection is prevented by config/option [rights] permit_delete_collection and not explicit allowed by permission 'D': %s", path)
return httputils.NOT_ALLOWED return httputils.NOT_ALLOWED
for i in item.get_all(): if self._hook.enabled:
hook_notification_item_list.append( for i in item.get_all():
HookNotificationItem( hook_notification_item_list.append(
notification_item_type=HookNotificationItemTypes.DELETE, HookNotificationItem(
path=access.path, notification_item_type=HookNotificationItemTypes.DELETE,
content=i.uid, path=access.path,
uid=i.uid, content=i.uid,
old_content=i.serialize(), # type: ignore content_type=i.name,
new_content=None uid=i.uid,
old_content=i.serialize(), # type: ignore
new_content=None
)
) )
)
xml_answer = xml_delete(base_prefix, path, item) xml_answer = xml_delete(base_prefix, path, item)
else: else:
assert item.collection is not None assert item.collection is not None
assert item.href is not None assert item.href is not None
hook_notification_item_list.append( if self._hook.enabled:
HookNotificationItem( hook_notification_item_list.append(
notification_item_type=HookNotificationItemTypes.DELETE, HookNotificationItem(
path=access.path, notification_item_type=HookNotificationItemTypes.DELETE,
content=item.uid, path=access.path,
uid=item.uid, content=item.uid,
old_content=item.serialize(), # type: ignore content_type=item.name,
new_content=None, uid=item.uid,
old_content=item.serialize(), # type: ignore
new_content=None,
)
) )
)
xml_answer = xml_delete( xml_answer = xml_delete(
base_prefix, path, item.collection, item.href) base_prefix, path, item.collection, item.href)
for notification_item in hook_notification_item_list: for notification_item in hook_notification_item_list: # Will be empty if hook not enabled
self._hook.notify(notification_item) self._hook.notify(notification_item)
headers = {"Content-Type": "text/xml; charset=%s" % self._encoding} headers = {"Content-Type": "text/xml; charset=%s" % self._encoding}
return client.OK, headers, self._xml_response(xml_answer), None return client.OK, headers, self._xml_response(xml_answer), None

View File

@@ -205,15 +205,17 @@ class ApplicationPartProppatch(ApplicationBase):
xml_content, xml_content,
encoding=self._encoding encoding=self._encoding
).decode(encoding=self._encoding) ).decode(encoding=self._encoding)
hook_notification_item = HookNotificationItem( if self._hook.enabled:
notification_item_type=HookNotificationItemTypes.CPATCH, hook_notification_item = HookNotificationItem(
path=access.path, notification_item_type=HookNotificationItemTypes.CPATCH,
content=content, path=access.path,
uid=None, content=content,
old_content=None, content_type=None, # Can't easily determine content type, won't trigger email hook
new_content=content uid=None,
) old_content=None,
self._hook.notify(hook_notification_item) new_content=content
)
self._hook.notify(hook_notification_item)
except ValueError as e: except ValueError as e:
# return better matching HTTP result in case errno is provided and catched # return better matching HTTP result in case errno is provided and catched
errno_match = re.search("\\[Errno ([0-9]+)\\]", str(e)) errno_match = re.search("\\[Errno ([0-9]+)\\]", str(e))

View File

@@ -306,28 +306,32 @@ class ApplicationPartPut(ApplicationBase):
href=path, href=path,
items=prepared_items, items=prepared_items,
props=props) props=props)
for item in prepared_items: # Only run the hook if the hook is enabled
# Try to grab the previously-existing item by href if self._hook.enabled:
existing_item = replaced_items.get(item.href, None) # type: ignore for item in prepared_items:
if existing_item: # Try to grab the previously-existing item by href
hook_notification_item = HookNotificationItem( existing_item = replaced_items.get(item.href, None) # type: ignore
notification_item_type=HookNotificationItemTypes.UPSERT, if existing_item:
path=access.path, hook_notification_item = HookNotificationItem(
content=existing_item.serialize(), notification_item_type=HookNotificationItemTypes.UPSERT,
uid=None, path=access.path,
old_content=existing_item.serialize(), content=existing_item.serialize(),
new_content=item.serialize() content_type=item.name,
) uid=None,
else: # We assume the item is new because it was not in the replaced_items old_content=existing_item.serialize(),
hook_notification_item = HookNotificationItem( new_content=item.serialize(),
notification_item_type=HookNotificationItemTypes.UPSERT, )
path=access.path, else: # We assume the item is new because it was not in the replaced_items
content=item.serialize(), hook_notification_item = HookNotificationItem(
uid=None, notification_item_type=HookNotificationItemTypes.UPSERT,
old_content=None, path=access.path,
new_content=item.serialize() content=item.serialize(),
) content_type=item.name,
self._hook.notify(hook_notification_item) uid=None,
old_content=None,
new_content=item.serialize()
)
self._hook.notify(hook_notification_item)
except ValueError as e: except ValueError as e:
logger.warning( logger.warning(
"Bad PUT request on %r (create_collection): %s", path, e, exc_info=True) "Bad PUT request on %r (create_collection): %s", path, e, exc_info=True)
@@ -345,15 +349,17 @@ class ApplicationPartPut(ApplicationBase):
try: try:
uploaded_item, replaced_item = parent_item.upload(href, prepared_item) uploaded_item, replaced_item = parent_item.upload(href, prepared_item)
etag = uploaded_item.etag etag = uploaded_item.etag
hook_notification_item = HookNotificationItem( if self._hook.enabled:
notification_item_type=HookNotificationItemTypes.UPSERT, hook_notification_item = HookNotificationItem(
path=access.path, notification_item_type=HookNotificationItemTypes.UPSERT,
content=prepared_item.serialize(), path=access.path,
uid=None, content=prepared_item.serialize(),
old_content=replaced_item.serialize() if replaced_item else None, content_type=prepared_item.name,
new_content=prepared_item.serialize() uid=None,
) old_content=replaced_item.serialize() if replaced_item else None,
self._hook.notify(hook_notification_item) new_content=prepared_item.serialize(),
)
self._hook.notify(hook_notification_item)
except ValueError as e: except ValueError as e:
# return better matching HTTP result in case errno is provided and catched # return better matching HTTP result in case errno is provided and catched
errno_match = re.search("\\[Errno ([0-9]+)\\]", str(e)) errno_match = re.search("\\[Errno ([0-9]+)\\]", str(e))

View File

@@ -33,6 +33,11 @@ class BaseHook:
""" """
self.configuration = configuration self.configuration = configuration
@property
def enabled(self) -> bool:
"""Check if this hook is enabled."""
raise NotImplementedError
def notify(self, notification_item): def notify(self, notification_item):
"""Upload a new or replace an existing item.""" """Upload a new or replace an existing item."""
raise NotImplementedError raise NotImplementedError
@@ -55,10 +60,12 @@ def _cleanup(path):
class HookNotificationItem: class HookNotificationItem:
def __init__(self, notification_item_type, path, content=None, uid=None, new_content=None, old_content=None): def __init__(self, notification_item_type, path, content=None, content_type=None, uid=None, new_content=None,
old_content=None):
self.type = notification_item_type.value self.type = notification_item_type.value
self.point = _cleanup(path) self.point = _cleanup(path)
self._content_legacy = content self._content_legacy = content
self.content_type = content_type
self.uid = uid self.uid = uid
self.new_content = new_content self.new_content = new_content
self.old_content = old_content self.old_content = old_content
@@ -67,6 +74,30 @@ class HookNotificationItem:
def content(self): # For backward compatibility def content(self): # For backward compatibility
return self._content_legacy or self.uid or self.new_content or self.old_content return self._content_legacy or self.uid or self.new_content or self.old_content
@property
def is_calendar_item(self) -> bool:
"""Check if this notification item is related to a VCALENDAR item."""
if not self.content_type:
return False
return self.content_type.upper().strip() == "VCALENDAR"
@property
def is_card_item(self) -> bool:
"""Check if this notification item is related to a VCARD item."""
if not self.content_type:
return False
return self.content_type.upper().strip() == "VCARD"
@property
def is_addressbook_item(self) -> bool:
"""Check if this notification item is related to a VADDRESSBOOK item."""
if not self.content_type:
return False
return self.content_type.upper().strip() == "VADDRESSBOOK"
@property @property
def replaces_existing_item(self) -> bool: def replaces_existing_item(self) -> bool:
"""Check if this notification item replaces/deletes an existing item.""" """Check if this notification item replaces/deletes an existing item."""

View File

@@ -930,12 +930,21 @@ class Hook(BaseHook):
self.email_config self.email_config
) )
@property
def enabled(self) -> bool:
"""Check if this hook is enabled (has a notify method)."""
return self.email_config.host is not None
def notify(self, notification_item) -> None: def notify(self, notification_item) -> None:
""" """
Entrypoint for processing a single notification item. Entrypoint for processing a single notification item.
Overrides default notify method from BaseHook. Overrides default notify method from BaseHook.
Triggered by Radicale when a notifiable event occurs (e.g. item added, updated or deleted) Triggered by Radicale when a notifiable event occurs (e.g. item added, updated or deleted)
""" """
# Skip any non-VCALENDAR items
if not notification_item.is_calendar_item:
return
if isinstance(notification_item, HookNotificationItem): if isinstance(notification_item, HookNotificationItem):
self._process_event_and_notify(notification_item) self._process_event_and_notify(notification_item)

View File

@@ -2,5 +2,10 @@ from radicale import hook
class Hook(hook.BaseHook): class Hook(hook.BaseHook):
@property
def enabled(self) -> bool:
"""Check if this hook is enabled."""
return False
def notify(self, notification_item): def notify(self, notification_item):
"""Notify nothing. Empty hook.""" """Notify nothing. Empty hook."""

View File

@@ -53,6 +53,11 @@ class Hook(hook.BaseHook):
logger.debug("Hook 'rabbitmq': _make_declare_queue_synced") logger.debug("Hook 'rabbitmq': _make_declare_queue_synced")
self._channel.queue_declare(queue=self._topic, durable=True, arguments={"x-queue-type": self._queue_type}) self._channel.queue_declare(queue=self._topic, durable=True, arguments={"x-queue-type": self._queue_type})
@property
def enabled(self) -> bool:
"""Check if this hook is enabled."""
return self._endpoint is not None
def notify(self, notification_item): def notify(self, notification_item):
if isinstance(notification_item, HookNotificationItem): if isinstance(notification_item, HookNotificationItem):
self._notify(notification_item, True) self._notify(notification_item, True)