2929_collation_to_caldav = collation_to_caldav
3030
3131
32+ def _is_not_defined_supported (features : Any , prop : str ) -> bool :
33+ """Check if is-not-defined search is supported for a specific property.
34+
35+ Checks the property-specific sub-feature (e.g. search.is-not-defined.category
36+ or search.is-not-defined.dtend) if one is defined, otherwise falls back to
37+ the parent search.is-not-defined feature.
38+
39+ The ``categories`` → ``category`` mapping exists because the feature is named
40+ after the singular form while the iCalendar property is plural.
41+ """
42+ from .compatibility_hints import FeatureSet
43+
44+ feature_prop = "category" if prop == "categories" else prop
45+ sub_feature = f"search.is-not-defined.{ feature_prop } "
46+ if sub_feature in FeatureSet .FEATURES :
47+ return features .is_supported (sub_feature )
48+ return features .is_supported ("search.is-not-defined" )
49+
50+
3251# Property filter attribute names used for cloning searchers with modified filters
3352_PROPERTY_FILTER_ATTRS = (
3453 "_property_filters" ,
@@ -229,13 +248,14 @@ def _search_impl(
229248
230249 ## Handle servers with broken component-type filtering (e.g., Bedework)
231250 comp_type_support = calendar .client .features .is_supported ("search.comp-type" , str )
232- if (
251+ no_comp_filter = (
233252 (self .comp_class or self .todo or self .event or self .journal )
234253 and comp_type_support == "broken"
235- and not _hacks
236254 and post_filter is not False
237- ):
238- _hacks = "no_comp_filter"
255+ )
256+ if no_comp_filter :
257+ if not _hacks :
258+ _hacks = "no_comp_filter"
239259 post_filter = True
240260
241261 ## Setting default value for post_filter
@@ -257,6 +277,29 @@ def _search_impl(
257277 if not self .start or not self .end :
258278 raise error .ReportError ("can't expand without a date range" )
259279
280+ ## special compatibility-case for servers that do not support text search at all
281+ ## (e.g. purelymail where both i;octet and i;ascii-casemap collations are unsupported).
282+ ## Remove all text-value filters and rely on client-side post_filter instead.
283+ if (
284+ not calendar .client .features .is_supported ("search.text" )
285+ and self ._property_filters
286+ and post_filter is not False
287+ ):
288+ text_filter_props = [
289+ prop for prop , op in self ._property_operator .items () if op != "undef"
290+ ]
291+ if text_filter_props :
292+ clone = self ._clone_without_filters (text_filter_props )
293+ objects = yield (
294+ SearchAction .RECURSIVE_SEARCH ,
295+ (clone , calendar , server_expand , split_expanded , props , xml , None , None ),
296+ )
297+ yield (
298+ SearchAction .RETURN ,
299+ self .filter (objects , post_filter , split_expanded , server_expand ),
300+ )
301+ return
302+
260303 ## special compatbility-case for servers that does not
261304 ## support category search properly
262305 if (
@@ -275,6 +318,31 @@ def _search_impl(
275318 )
276319 return
277320
321+ ## special compatibility-case for servers that do not support is-not-defined
322+ ## for specific properties (e.g. search.is-not-defined.category or .dtend)
323+ if post_filter is not False :
324+ undef_props_without_support = [
325+ prop
326+ for prop , op in self ._property_operator .items ()
327+ if op == "undef" and not _is_not_defined_supported (calendar .client .features , prop )
328+ ]
329+ if undef_props_without_support :
330+ clone = self ._clone_without_filters (undef_props_without_support )
331+ objects = yield (
332+ SearchAction .RECURSIVE_SEARCH ,
333+ (clone , calendar , server_expand , split_expanded , props , xml , None , None ),
334+ )
335+ yield (
336+ SearchAction .RETURN ,
337+ self .filter (
338+ objects ,
339+ post_filter = True ,
340+ split_expanded = split_expanded ,
341+ server_expand = server_expand ,
342+ ),
343+ )
344+ return
345+
278346 ## special compatibility-case for servers that do not support substring search
279347 if (
280348 not calendar .client .features .is_supported ("search.text.substring" )
@@ -353,7 +421,8 @@ def _search_impl(
353421 clone .expand = False
354422
355423 if (
356- calendar .client .features .is_supported ("search.text" )
424+ not no_comp_filter
425+ and calendar .client .features .is_supported ("search.text" )
357426 and calendar .client .features .is_supported ("search.combined-is-logical-and" )
358427 and (
359428 not calendar .client .features .is_supported (
@@ -440,6 +509,35 @@ def _search_impl(
440509 yield (SearchAction .RETURN , result )
441510 return
442511
512+ ## If _hacks=="insist" and still no results despite having text property
513+ ## filters, the server may not support text search (e.g. purelymail,
514+ ## CCS with i;octet collation). Retry without the text filters and rely
515+ ## on client-side post_filter (which is guaranteed True in get_object_by_uid).
516+ if not objects and _hacks == "insist" and self ._property_filters :
517+ non_undef_filters = [
518+ prop for prop , op in self ._property_operator .items () if op != "undef"
519+ ]
520+ if non_undef_filters :
521+ clone = self ._clone_without_filters (non_undef_filters )
522+ result = yield (
523+ SearchAction .RECURSIVE_SEARCH ,
524+ (
525+ clone ,
526+ calendar ,
527+ server_expand ,
528+ split_expanded ,
529+ props ,
530+ orig_xml ,
531+ None ,
532+ None ,
533+ ),
534+ )
535+ yield (
536+ SearchAction .RETURN ,
537+ self .filter (result , post_filter , split_expanded , server_expand ),
538+ )
539+ return
540+
443541 # Post-process: load objects
444542 obj2 = []
445543 for o in objects :
0 commit comments