jmap: request the Thread property the spec names
Thread/get sorts and reads emailIds (RFC 8621 §3), not emails —
the fixtures shared the invented name, so nothing contradicted it
until Cyrus did.

Assisted-by: GLM-5.3 via pi
change xlroqoyuvwzzopuzmxqruzspyxkmsyow
commit 4cfbbb5d2d9509e81edb814c43b4a386155d06df
author Alpha Chen <alpha@kejadlen.dev>
date
parent suplxnks
diff --git a/src/jmap.rs b/src/jmap.rs
index f7860b8..95503f0 100644
--- a/src/jmap.rs
+++ b/src/jmap.rs
@@ -177,7 +177,7 @@ struct Query {
 #[derive(Deserialize)]
 #[serde(rename_all = "camelCase")]
 struct ThreadEmails {
-    emails: Vec<String>,
+    email_ids: Vec<String>,
 }
 
 /// Pulls one page of ids out of an `Email/query` response.
@@ -678,10 +678,10 @@ struct GetThreads<'a> {
 #[serde(rename_all = "camelCase")]
 enum ThreadProperty {
     Id,
-    Emails,
+    EmailIds,
 }
 
-const THREAD_PROPERTIES: &[ThreadProperty] = &[ThreadProperty::Id, ThreadProperty::Emails];
+const THREAD_PROPERTIES: &[ThreadProperty] = &[ThreadProperty::Id, ThreadProperty::EmailIds];
 
 /// `Foo/changes` arguments (RFC 8620 §5.2).
 #[derive(Debug, Serialize)]
@@ -1002,7 +1002,12 @@ impl Client {
                 )
                 .await?;
             for thread in parse_list::<ThreadEmails>("Thread/get", args)? {
-                wanted.extend(thread.emails.into_iter().filter(|id| !known.contains(id)));
+                wanted.extend(
+                    thread
+                        .email_ids
+                        .into_iter()
+                        .filter(|id| !known.contains(id)),
+                );
             }
         }
         for chunk in wanted.chunks(PAGE) {
@@ -1334,7 +1339,7 @@ mod tests {
         .unwrap();
         assert_eq!(
             threads,
-            json!({"accountId": "a", "ids": ["t1"], "properties": ["id", "emails"]})
+            json!({"accountId": "a", "ids": ["t1"], "properties": ["id", "emailIds"]})
         );
 
         let changes = serde_json::to_value(GetChanges {
diff --git a/tests/jmap.rs b/tests/jmap.rs
index 78d1dee..52e5ba4 100644
--- a/tests/jmap.rs
+++ b/tests/jmap.rs
@@ -292,11 +292,11 @@ fn emails_of(account: &str) -> Vec<Value> {
 fn threads_of(account: &str) -> Vec<Value> {
     match account {
         HOUSEHOLD => vec![
-            json!({"id": "T-roofer", "emails": ["E-roofer1", "E-roofer2", "E-reply1"]}),
-            json!({"id": "T-school", "emails": ["E-school", "E-reply2", "E-draft"]}),
-            json!({"id": "T-ancient", "emails": ["E-reply3"]}),
+            json!({"id": "T-roofer", "emailIds": ["E-roofer1", "E-roofer2", "E-reply1"]}),
+            json!({"id": "T-school", "emailIds": ["E-school", "E-reply2", "E-draft"]}),
+            json!({"id": "T-ancient", "emailIds": ["E-reply3"]}),
         ],
-        ELI => vec![json!({"id": "T-practice", "emails": ["E-practice"]})],
+        ELI => vec![json!({"id": "T-practice", "emailIds": ["E-practice"]})],
         _ => Vec::new(),
     }
 }
@@ -540,6 +540,16 @@ async fn api(world: axum::extract::State<World>, headers: HeaderMap, body: Strin
                        .collect::<Vec<_>>()})
         }
         "Thread/get" => {
+            // A Thread's only properties are id and emailIds (RFC 8621
+            // §3); servers answer anything else with invalidArguments.
+            if let Some(properties) = args["properties"].as_array() {
+                for property in properties {
+                    assert!(
+                        matches!(property.as_str(), Some("id") | Some("emailIds")),
+                        "unknown Thread property {property}"
+                    );
+                }
+            }
             let wanted: Vec<&str> = args["ids"]
                 .as_array()
                 .map(|ids| ids.iter().filter_map(Value::as_str).collect())