Skip to main content

teams_cli/
delivery.rs

1//! Delivery evidence is distinct from request success. Unknown never means safe to retry.
2use serde::Serialize;
3use serde_json::{Value, json};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
6#[serde(rename_all = "snake_case")]
7pub enum State {
8    Submitted,
9    Queued,
10    Delivered,
11    Failed,
12    Unknown,
13}
14
15impl State {
16    pub fn from_message(message: &Value) -> Self {
17        match message["status"]
18            .as_str()
19            .map(str::to_ascii_lowercase)
20            .as_deref()
21        {
22            Some("delivered") => Self::Delivered,
23            Some("queued" | "pending" | "sending") => Self::Queued,
24            Some("failed") => Self::Failed,
25            // An unrecognised Teams status is deliberately left unknown.
26            Some(_) => Self::Unknown,
27            None if message["source"] == "desktop" => Self::Submitted,
28            None => Self::Unknown,
29        }
30    }
31    pub fn notice(self) -> &'static str {
32        match self {
33            Self::Submitted => "Submitted to Teams; delivery is not confirmed.",
34            Self::Queued => "Queued in Teams; delivery is not confirmed.",
35            Self::Delivered => "Delivered.",
36            Self::Failed => "Teams reports a failed send; check before resending.",
37            Self::Unknown => "Delivery is unknown; check before resending.",
38        }
39    }
40}
41
42pub fn client_message_id() -> String {
43    use rand::Rng;
44    rand::rng()
45        .random_range(100_000_000_000_000_000u64..1_000_000_000_000_000_000)
46        .to_string()
47}
48
49pub fn valid_client_message_id(id: &str) -> bool {
50    id.len() == 18 && id.bytes().all(|b| b.is_ascii_digit())
51}
52
53/// Reports only exact ID evidence from a bounded newest cache page. Absence is unknown.
54pub fn reconcile(chat: &str, id: &str, messages: &[Value]) -> Value {
55    let found = messages.iter().find(|m| m["clientMessageId"] == id);
56    json!({"chatId":chat,"clientMessageId":id,"found":found.is_some(),
57        "id":found.and_then(|m| m["id"].as_str()),
58        "status":found.and_then(|m|m["status"].as_str()),
59        "delivery_state":found.map(State::from_message).unwrap_or(State::Unknown),
60        "source":"desktop_cache","scope":"newest_50_cached_messages","retryable":false})
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    #[test]
67    fn success_is_not_delivery_and_unrecognised_labels_remain_unknown() {
68        for (message, expected) in [
69            (json!({"source":"desktop"}), State::Submitted),
70            (json!({"status":"Delivered"}), State::Delivered),
71            (json!({"status":"Queued"}), State::Queued),
72            (json!({"status":"SomeFutureStatus"}), State::Unknown),
73            (json!({"status":"Failed"}), State::Failed),
74        ] {
75            assert_eq!(State::from_message(&message), expected);
76        }
77    }
78    #[test]
79    fn readback_matches_only_the_original_client_id_and_never_infers_failure() {
80        let messages = vec![
81            json!({"id":"1","clientMessageId":"100000000000000001","status":"Delivered","body":{"content":"private"}}),
82        ];
83        let found = reconcile("chat", "100000000000000001", &messages);
84        assert_eq!(found["delivery_state"], "delivered");
85        assert!(found.get("body").is_none());
86        let missing = reconcile("chat", "100000000000000002", &messages);
87        assert_eq!(missing["found"], false);
88        assert_eq!(missing["delivery_state"], "unknown");
89        assert_eq!(missing["retryable"], false);
90    }
91}