{"record":{"id":"e6d1570b64059a9e","repo":"googleworkspace/cli","slug":"message-is-missing-message-id-header","errorCode":null,"errorMessage":"Message is missing Message-ID header","messagePattern":"Message is missing Message-ID header","errorType":"validation","errorClass":"GwsError","httpStatus":null,"severity":"error","filePath":"crates/google-workspace-cli/src/helpers/gmail/mod.rs","lineNumber":323,"sourceCode":"        .unwrap_or(\"\")\n        .to_string();\n\n    let parsed_headers = msg\n        .get(\"payload\")\n        .and_then(|p| p.get(\"headers\"))\n        .and_then(|h| h.as_array())\n        .map(|headers| parse_message_headers(headers))\n        .unwrap_or_default();\n\n    if parsed_headers.from.is_empty() {\n        return Err(GwsError::Other(anyhow::anyhow!(\n            \"Message is missing From header\"\n        )));\n    }\n\n    let message_id = strip_angle_brackets(&parsed_headers.message_id);\n    if message_id.is_empty() {\n        return Err(GwsError::Other(anyhow::anyhow!(\n            \"Message is missing Message-ID header\"\n        )));\n    }\n\n    let PayloadContents {\n        body_text: extracted_text,\n        body_html,\n        parts: original_parts,\n    } = msg\n        .get(\"payload\")\n        .map(extract_payload_contents)\n        .unwrap_or_default();\n\n    let body_text = extracted_text.unwrap_or(snippet);\n\n    // Parse references: split on whitespace and strip any angle brackets, producing bare IDs\n    let references = parsed_headers\n        .references","sourceCodeStart":305,"sourceCodeEnd":341,"githubUrl":"https://github.com/googleworkspace/cli/blob/a3768d0e82ad83cca2da97724e46bea4ff0e6dbd/crates/google-workspace-cli/src/helpers/gmail/mod.rs#L305-L341","documentation":"Raised right after the From check in `parse_original_message`: the parsed headers contained no usable `Message-ID` (the value is run through `strip_angle_brackets` and comes back empty). Message-ID is required because `+reply`/`+reply-all` set `In-Reply-To`/`References` threading headers, and `+forward` preserves the original ID — without it the helper cannot build a standards-compliant continuation.","triggerScenarios":"Replying to mail generated by broken MTAs, some automated senders, or spam that omit Message-ID; drafts never sent; messages fetched where the header exists but is empty (`<>`) so angle-bracket stripping yields an empty string.","commonSituations":"`gws gmail +reply --message-id <ID>` against machine-generated notifications, calendar digests, or mail imported from legacy systems; reply chains that break threading on the recipient's client.","solutions":["Verify with `gws gmail users.messages get ... | jq '.payload.headers[] | select(.name==\"Message-ID\")'` that the header is truly absent or empty.","If absent, the message cannot be threaded — reply manually or from a different client for that one message.","Keep the CLI updated; header parsing changes land in `helpers/gmail/mod.rs`.","If you control the sending side, fix the origin MTA to always emit a Message-ID (RFC 5322 requires one)."],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":"// Check Message-ID presence (non-empty after <> stripping) before invoking reply\nlet mid = msg.pointer(\"/payload/headers\")\n    .and_then(|h| h.as_array())\n    .and_then(|hs| hs.iter().find(|h| h.get(\"name\").and_then(|n| n.as_str()).map(|n| n.eq_ignore_ascii_case(\"Message-ID\")).unwrap_or(false)))\n    .and_then(|h| h.get(\"value\").and_then(|v| v.as_str()))\n    .map(|v| v.trim().trim_start_matches('<').trim_end_matches('>'));\nif mid.map_or(true, |m| m.is_empty()) {\n    eprintln!(\"No usable Message-ID; thread will not be continuable\");\n}","typeGuard":"fn has_message_id(msg: &serde_json::Value) -> bool {\n    msg.pointer(\"/payload/headers\")\n        .and_then(|h| h.as_array())\n        .map(|hs| hs.iter().any(|h| {\n            let is_mid = h.get(\"name\").and_then(|n| n.as_str()).map(|n| n.eq_ignore_ascii_case(\"Message-ID\")).unwrap_or(false);\n            let non_empty = h.get(\"value\").and_then(|v| v.as_str()).map(|v| !v.trim().trim_matches(|c| c == '<' || c == '>').is_empty()).unwrap_or(false);\n            is_mid && non_empty\n        }))\n        .unwrap_or(false)\n}","tryCatchPattern":"match run_reply(message_id).await {\n    Err(GwsError::Other(e)) if e.to_string().contains(\"Message-ID\") => { skip_and_log(message_id); }\n    rest => rest?,\n}","preventionTips":["Skip Message-ID-less messages in automated reply pipelines rather than aborting the batch.","Remember the value `<>` counts as missing — strip brackets in your own guard exactly like strip_angle_brackets does.","Ensure origin systems you control always emit a Message-ID header (RFC 5322)."],"tags":["gmail","email-headers","threading","reply","message-id"],"backgroundTag":"missing-email-header","analyzedSha":"a3768d0e82ad83cca2da97724e46bea4ff0e6dbd","analyzedAt":"2026-08-16T19:51:46.516Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}