Skip to main content

imsg_map/client/
messages.rs

1//! Message CRUD: listing, fetch, send, and status updates.
2
3use bytes::Bytes;
4use formats::bmessage::BMessage;
5use futures::SinkExt;
6use obex_core::{
7    client::{ObexClient, ObexError},
8    headers::Header,
9};
10use tokio::io::{AsyncRead, AsyncWrite};
11
12use super::MapClient;
13use crate::{
14    messages::{ListMessagesFilter, MessageEntry},
15    params::{
16        get_message_params, push_message_params, set_message_status_params,
17        INDICATOR_DELETED_STATUS, INDICATOR_READ_STATUS,
18    },
19    MapError, MessageStatus,
20};
21
22impl<T: AsyncRead + AsyncWrite + Unpin> MapClient<T> {
23    /// Caller must navigate to the target folder via [`Self::set_folder`] before calling this.
24    /// Sends a `GetMessagesListing` GET with no Name header — the device lists the current OBEX
25    /// working directory. Accumulates body chunks across CONTINUE responses before parsing.
26    ///
27    /// # Errors
28    ///
29    /// Returns [`MapError`] if the transport fails, the server rejects the request, or the
30    /// response XML is malformed.
31    pub async fn list_messages(
32        &mut self,
33        filter: &ListMessagesFilter,
34    ) -> Result<Vec<MessageEntry>, MapError> {
35        let req = self.obex.get_request(
36            b"x-bt/MAP-msg-listing\x00",
37            None,
38            Some(filter.to_app_params()?),
39        )?;
40        self.transport.send(req).await?;
41        let body = self.collect_body().await?;
42        Ok(crate::xml::parse_message_listing(&body)?)
43    }
44
45    /// Sends a `GetMessage` GET with `Type: x-bt/message` and `Charset=UTF-8`. Accumulates body
46    /// chunks across CONTINUE responses before parsing.
47    ///
48    /// # Errors
49    ///
50    /// Returns [`MapError::InvalidInput`] if `handle` is empty or contains CR or LF — it lands
51    /// verbatim in the OBEX Name header. Returns [`MapError`] if the transport fails, the server
52    /// rejects the request, the response body exceeds 4 MiB, is not valid UTF-8, or the bMessage
53    /// is malformed.
54    pub async fn get_message(&mut self, handle: &str) -> Result<BMessage, MapError> {
55        if handle.is_empty() {
56            return Err(MapError::InvalidInput("handle must not be empty"));
57        }
58        if handle.contains(['\r', '\n']) {
59            return Err(MapError::InvalidInput("handle must not contain CR or LF"));
60        }
61        let req =
62            self.obex.get_request(b"x-bt/message\x00", Some(handle), Some(get_message_params()))?;
63        self.transport.send(req).await?;
64        let body = self.collect_body().await?;
65        let text = std::str::from_utf8(&body).map_err(|_| MapError::InvalidEncoding)?;
66        Ok(BMessage::parse(text)?)
67    }
68
69    /// Sends `text` as an outbound SMS to `phone` via MAP `PushMessage`. Returns the opaque
70    /// handle assigned by the remote, suitable for passing to `set_message_status`. Caller must
71    /// have navigated to outbox via `set_folder(Folder::Outbox)` first.
72    ///
73    /// # Errors
74    ///
75    /// Returns [`MapError`] if encoding fails, the transport closes, the server rejects the
76    /// request, or the OK response contains no Name header.
77    pub async fn push_message(&mut self, phone: &str, text: &str) -> Result<String, MapError> {
78        if phone.contains(['\r', '\n']) {
79            return Err(MapError::InvalidInput("phone must not contain CR or LF"));
80        }
81        let body = BMessage::outbound_sms(phone, text).encode();
82        let body_bytes = body.as_bytes();
83        let len = u32::try_from(body_bytes.len()).map_err(|_| ObexError::BodyTooLarge)?;
84        let req = self.obex.put_final_request(
85            b"x-bt/message\x00",
86            vec![
87                Header::Name(String::new()),
88                Header::Length(len),
89                Header::AppParams(push_message_params()),
90                Header::EndOfBody(Bytes::copy_from_slice(body_bytes)),
91            ],
92        )?;
93        self.transport.send(req).await?;
94        let rsp_bytes = Self::recv(&mut self.transport).await?;
95        let rsp = ObexClient::parse_response(&rsp_bytes)?;
96        if !rsp.opcode.is_ok() {
97            return Err(MapError::ServerError(rsp.opcode.to_byte()));
98        }
99        rsp.header_name().ok_or(MapError::MissingHandle)
100    }
101
102    /// Marks the message identified by `handle` as `status` (read or unread) via MAP
103    /// `SetMessageStatus`. Caller must navigate to the containing folder via `set_folder` first.
104    ///
105    /// # Errors
106    ///
107    /// Returns [`MapError::InvalidInput`] if `handle` is empty or contains CR or LF.
108    /// Returns [`MapError::ServerError`] if the remote returns a non-OK response.
109    /// Returns [`MapError::Obex`] or [`MapError::Transport`] on lower-layer failure.
110    pub async fn set_message_status_read(
111        &mut self,
112        handle: &str,
113        status: MessageStatus,
114    ) -> Result<(), MapError> {
115        let value = match status {
116            MessageStatus::Read => 0x01_u8,
117            MessageStatus::Unread => 0x00_u8,
118        };
119        self.do_set_message_status(handle, INDICATOR_READ_STATUS, value).await
120    }
121
122    /// Marks the message identified by `handle` as deleted (`true`) or undeleted (`false`) via
123    /// MAP `SetMessageStatus`. Caller must navigate to the containing folder via `set_folder` first.
124    ///
125    /// # Errors
126    ///
127    /// Returns [`MapError::InvalidInput`] if `handle` is empty or contains CR or LF.
128    /// Returns [`MapError::ServerError`] if the remote returns a non-OK response.
129    /// Returns [`MapError::Obex`] or [`MapError::Transport`] on lower-layer failure.
130    pub async fn set_message_status_deleted(
131        &mut self,
132        handle: &str,
133        deleted: bool,
134    ) -> Result<(), MapError> {
135        self.do_set_message_status(handle, INDICATOR_DELETED_STATUS, u8::from(deleted)).await
136    }
137
138    async fn do_set_message_status(
139        &mut self,
140        handle: &str,
141        indicator: u8,
142        value: u8,
143    ) -> Result<(), MapError> {
144        if handle.is_empty() {
145            return Err(MapError::InvalidInput("handle must not be empty"));
146        }
147        if handle.contains(['\r', '\n']) {
148            return Err(MapError::InvalidInput("handle must not contain CR or LF"));
149        }
150        let req = self.obex.put_final_request(
151            b"x-bt/messageStatus\x00",
152            vec![
153                Header::Name(handle.to_owned()),
154                Header::AppParams(Bytes::from(set_message_status_params(indicator, value))),
155            ],
156        )?;
157        self.transport.send(req).await?;
158        let rsp_bytes = Self::recv(&mut self.transport).await?;
159        let rsp = ObexClient::parse_response(&rsp_bytes)?;
160        if !rsp.opcode.is_ok() {
161            return Err(MapError::ServerError(rsp.opcode.to_byte()));
162        }
163        Ok(())
164    }
165}