1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
//! RFC 8620 §5.5 generic filter types for JMAP `/query` methods.
//!
//! Provides [`Filter`], [`FilterOperator`], and [`Operator`].
//! Object-specific filter conditions (e.g. `EmailFilterCondition`) are
//! defined in their respective type crates.
//!
//! # Filter algebra is excluded from extras preservation
//!
//! The filter algebra defined in this module is **intentionally not extensible**
//! via the workspace "extras preservation" policy. See [`Filter`],
//! [`FilterOperator`], and [`Operator`] for details. The same exclusion applies
//! to every per-object `FilterCondition` / `Comparator` / `ComparatorProperty`
//! type in the downstream `jmap-*-types` crates (see workspace `AGENTS.md`,
//! bd JMAP-lbdy "Decision: filter algebra excluded").
use ;
/// Logical operator for combining filter conditions (RFC 8620 §5.5).
///
/// # Excluded from extras preservation
///
/// This enum is **out of scope** for the workspace extras-preservation policy:
/// it carries no `Other(String)` catch-all variant, and backends must
/// dispatch on its known variants (`AND`, `OR`, `NOT`) to evaluate a filter
/// tree. An `Other` operator would be meaningless — a server that cannot
/// interpret the operator cannot evaluate the filter, and silently round-
/// tripping it back to the client would yield wrong query results.
///
/// More broadly, filter algebra (this enum and the per-object
/// `FilterCondition` / `Comparator` types) is excluded because unrecognised
/// filter clauses are a query-correctness hazard: silently dropping or
/// round-tripping a clause the server does not understand can return the
/// wrong set of records to the client without any error signal.
///
/// ## What to do instead
///
/// **IETF-track path.** Vendors who need both capability-level declaration
/// and filterability for custom fields should use
/// `draft-ietf-jmap-metadata` (capability URI
/// `urn:ietf:params:jmap:metadata`), which defines a `Metadata` / `Annotation`
/// companion object keyed by `(relatedType, relatedId)` with schema discovery
/// via the capability's `metadataTypes` / `maxDepth` properties and a
/// `Metadata/query` filter. Implemented in `jmap-metadata-types`,
/// `jmap-metadata-server`, and `jmap-metadata-client` (bd JMAP-06zp).
///
/// **Pre-IETF escape.** Vendors who cannot wait for the metadata draft can
/// either escape the filter tree to `serde_json::Value` or fork the
/// per-crate `FilterCondition` type. See
/// `crate-jmap-calendars-types/PLAN.md` for the hybrid sloppy-value
/// pattern.
///
/// Cross-reference: bd JMAP-lbdy "Decision: filter algebra excluded".
/// A filter node: either a logical operator combining sub-filters, or a
/// type-specific condition object (RFC 8620 §5.5).
///
/// Serializes as an untagged union. The presence of the `"operator"` key
/// distinguishes `Filter::Operator` from `Filter::Condition`.
///
/// **Variant ordering is critical**: `Operator` is listed before `Condition`
/// because serde untagged tries variants in declaration order.
/// `FilterOperator<T>` requires an `"operator"` field and fails fast without
/// it, allowing the deserializer to fall through to `Condition(T)`.
///
/// # Malformed-operator hazard (silent fallthrough)
///
/// The untagged-enum dispatch combined with the per-crate `FilterCondition`
/// types being structs of all-`Option` fields without
/// `#[serde(deny_unknown_fields)]` creates a query-correctness hazard:
/// a client clause whose `operator` key is misspelled (e.g.
/// `{"opperator":"AND","conditions":[]}` or `{"Operator":"AND","conditions":[]}`)
/// fails to deserialize as [`FilterOperator`] (the spelling does not match
/// the typed field) and falls through to `Filter::Condition(T)`. The
/// fallthrough variant then deserializes as `T::default()` (all fields
/// `None`), which semantically means "no constraint" and therefore
/// **matches every record**. The malformed clause produces no
/// deserialization error and no query error — the query just silently
/// returns the full result set.
///
/// `#[serde(deny_unknown_fields)]` cannot be added to the `T`
/// implementations because it interacts badly with `#[serde(untagged)]`
/// (see `jmap-mail-types/src/query.rs` for the in-tree warning).
///
/// **Server-side defenses** (the canonical mitigations live in server
/// crates, not here):
///
/// 1. Validate the parsed filter tree. After deserialization, walk the
/// tree and reject any `Filter::Condition(t)` whose `t` has every
/// field unset — that is a "match all" pre-image and almost certainly
/// a malformed client clause. Map to RFC 8620 §5.5
/// `unsupportedFilter`.
/// 2. Validate the raw JSON before / instead of relying on the typed
/// deserialization. Look for `conditions` adjacent to a non-`operator`
/// key, or `operator` adjacent to a non-`conditions` key, or any
/// object containing neither `operator` nor a recognised
/// `T::FieldName`. Reject with `unsupportedFilter`.
///
/// Clients writing query filters should never rely on "no-error" as a
/// signal of acceptance; always check that the response shape matches
/// what they asked for.
///
/// Regression test in this crate: see
/// `filter_with_typoed_operator_silently_decodes_as_match_all_condition`.
///
/// # Excluded from extras preservation
///
/// This type is **out of scope** for the workspace extras-preservation
/// policy: it carries no flatten-extras `extra` field, and the per-object
/// condition type `T` is also expected to be non-extensible. Filter clauses
/// the server does not understand are a query-correctness hazard — silently
/// preserving an unrecognised clause and round-tripping it back to the
/// client can return the wrong set of records with no error signal.
///
/// ## What to do instead
///
/// **IETF-track path.** Vendors who need both capability-level declaration
/// and filterability for custom fields should use
/// `draft-ietf-jmap-metadata` (capability URI
/// `urn:ietf:params:jmap:metadata`), which defines a filterable
/// `Metadata` / `Annotation` companion object. Implemented in `jmap-metadata-types`,
/// `jmap-metadata-server`, and `jmap-metadata-client` (bd JMAP-06zp).
///
/// **Pre-IETF escape.** Vendors who cannot wait for the metadata draft can
/// either escape the filter tree to `serde_json::Value` or fork the
/// per-crate `FilterCondition` type. See
/// `crate-jmap-calendars-types/PLAN.md` for the hybrid sloppy-value
/// pattern.
///
/// Cross-reference: bd JMAP-lbdy "Decision: filter algebra excluded".
/// Logical combination of filters (RFC 8620 §5.5).
///
/// # Excluded from extras preservation
///
/// This type is **out of scope** for the workspace extras-preservation
/// policy: it carries no flatten-extras `extra` field, and its
/// [`Operator`] field is a closed control enum that backends must dispatch
/// on. See [`Operator`] and [`Filter`] for the rationale and for the two
/// recommended paths (`draft-ietf-jmap-metadata`, bd JMAP-06zp; or the
/// pre-IETF sloppy-value escape).
///
/// Cross-reference: bd JMAP-lbdy "Decision: filter algebra excluded".