jmap: request email bodies with spec arguments
Email/get has no fetchData argument — Fastmail rejects unknown
arguments with invalidArguments — and textBody/bodyValues were never
in the properties list, so bodies fell back to previews anyway.

Assisted-by: GLM-5.3 via pi
change wqmnvrktvoyzlompuqvmzrzmsykklyry
commit 94d544db9ae0a490558d29a5196f1b3aadd28c0c
author Alpha Chen <alpha@kejadlen.dev>
date
parent ymmktylz
diff --git a/src/jmap.rs b/src/jmap.rs
index 64c3cb3..025896e 100644
--- a/src/jmap.rs
+++ b/src/jmap.rs
@@ -620,9 +620,9 @@ impl Client {
                 json!({"accountId": account_id, "ids": ids,
                     "properties": ["id", "threadId", "messageId", "mailboxIds",
                         "receivedAt", "sentAt", "subject", "from", "to", "cc", "bcc",
-                        "preview"],
-                    "fetchData": {"bodyProperties": ["partId", "type"],
-                        "bodyValues": ["text/plain"], "textBody": true}}),
+                        "preview", "textBody", "bodyValues"],
+                    "bodyProperties": ["partId", "type"],
+                    "fetchTextBodyValues": true}),
             )
             .await?;
         parse_stateful_list("Email/get", args)
diff --git a/tests/jmap.rs b/tests/jmap.rs
index 4ff46ef..75b3848 100644
--- a/tests/jmap.rs
+++ b/tests/jmap.rs
@@ -482,15 +482,48 @@ async fn api(world: axum::extract::State<World>, headers: HeaderMap, body: Strin
                    "total": ids.len()})
         }
         "Email/get" => {
+            // Servers answer arguments outside the /get and body-
+            // fetching set (RFC 8621 §4.2) with invalidArguments.
+            let known = [
+                "accountId",
+                "ids",
+                "properties",
+                "#returnState",
+                "bodyProperties",
+                "fetchTextBodyValues",
+                "fetchHTMLBodyValues",
+                "fetchAllBodyValues",
+                "maxBodyValueBytes",
+            ];
+            for key in args.as_object().unwrap().keys() {
+                assert!(
+                    known.contains(&key.as_str()),
+                    "Email/get got unknown argument {key}"
+                );
+            }
             let wanted: Vec<&str> = args["ids"]
                 .as_array()
                 .map(|ids| ids.iter().filter_map(Value::as_str).collect())
                 .unwrap_or_default();
+            // Like a real server, only requested properties come back.
+            let properties: Vec<&str> = args["properties"]
+                .as_array()
+                .map(|ps| ps.iter().filter_map(Value::as_str).collect())
+                .unwrap_or_default();
             json!({"accountId": account, "state": mail.email_states.last().unwrap().0,
                    "notFound": [],
                    "list": mail.emails.iter()
                        .filter(|e| wanted.contains(&e["id"].as_str().expect("an id")))
-                       .cloned().collect::<Vec<_>>()})
+                       .map(|e| {
+                           let mut filtered = json!({});
+                           for property in &properties {
+                               if let Some(value) = e.get(*property) {
+                                   filtered[*property] = value.clone();
+                               }
+                           }
+                           filtered
+                       })
+                       .collect::<Vec<_>>()})
         }
         "Thread/get" => {
             let wanted: Vec<&str> = args["ids"]