Skip to main content

imsg_map/
client.rs

1//! MAP client — OBEX session setup, SETPATH sequencing, and request dispatch.
2
3use bytes::Bytes;
4use formats::bmessage::BMessage;
5use formats::xml::FolderListing;
6use futures::{SinkExt, StreamExt};
7use obex_core::{
8    client::{ObexClient, ObexError},
9    headers::Header,
10};
11use obex_core::{wrap, ObexTransport};
12use tokio::io::{AsyncRead, AsyncWrite};
13
14use crate::{
15    folders::Folder,
16    messages::{ListMessagesFilter, MessageEntry},
17    params::{
18        get_message_params, push_message_params, set_message_status_params,
19        set_notification_registration_params, INDICATOR_DELETED_STATUS, INDICATOR_READ_STATUS,
20    },
21    MapError, MessageStatus,
22};
23
24const MAP_UUID: [u8; 16] = [
25    0xbb, 0x58, 0x2b, 0x40, 0x42, 0x0c, 0x11, 0xdb, 0xb0, 0xde, 0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66,
26];
27
28const MAX_BODY_BYTES: usize = 4 * 1024 * 1024;
29
30/// MAP session over an OBEX transport. Owns the OBEX state machine and the framed I/O.
31///
32/// Obtain via [`MapClient::connect`].
33pub struct MapClient<T> {
34    obex: ObexClient,
35    transport: ObexTransport<T>,
36    // SETPATH depth confirmed by server; 0 = root, max 3 (telecom/msg/<folder>).
37    depth: u8,
38}
39
40impl<T: AsyncRead + AsyncWrite + Unpin> MapClient<T> {
41    /// Sends the MAP UUID as the OBEX `Target` header and validates the server's response.
42    ///
43    /// # Errors
44    ///
45    /// Returns [`MapError`] if the transport fails, packet encoding fails, the server rejects
46    /// the connection, or the response omits the `ConnectionId` header.
47    pub async fn connect(stream: T) -> Result<Self, MapError> {
48        let mut transport = wrap(stream);
49        let mut obex = ObexClient::new();
50        let req = ObexClient::connect_request(&MAP_UUID)?;
51        transport.send(req).await?;
52        let rsp = Self::recv(&mut transport).await?;
53        obex.handle_connect_response(&rsp)?;
54        Ok(Self { obex, transport, depth: 0 })
55    }
56
57    /// `segment` is a single path component, not a slash-joined path — iOS requires one
58    /// SETPATH per level. Does not validate that `segment` names an existing folder.
59    ///
60    /// # Errors
61    ///
62    /// Returns [`MapError`] if the request fails to encode, the transport closes, or the
63    /// server returns a non-OK response.
64    pub(crate) async fn setpath(&mut self, segment: &str) -> Result<(), MapError> {
65        let req = self.obex.setpath_request(segment)?;
66        self.transport.send(req).await?;
67        let rsp_bytes = Self::recv(&mut self.transport).await?;
68        let rsp = ObexClient::parse_response(&rsp_bytes)?;
69        if !rsp.opcode.is_ok() {
70            return Err(MapError::ServerError(rsp.opcode.to_byte()));
71        }
72        self.depth = self.depth.saturating_add(1);
73        Ok(())
74    }
75
76    /// Decrements `depth` on success. Does not check whether depth is already zero.
77    ///
78    /// # Errors
79    ///
80    /// Returns [`MapError`] if the request fails to encode, the transport closes, or the
81    /// server returns a non-OK response.
82    pub(crate) async fn setpath_up(&mut self) -> Result<(), MapError> {
83        debug_assert!(self.depth > 0, "setpath_up called at root");
84        let req = self.obex.setpath_backup_request()?;
85        self.transport.send(req).await?;
86        let rsp_bytes = Self::recv(&mut self.transport).await?;
87        let rsp = ObexClient::parse_response(&rsp_bytes)?;
88        if !rsp.opcode.is_ok() {
89            return Err(MapError::ServerError(rsp.opcode.to_byte()));
90        }
91        self.depth = self.depth.saturating_sub(1);
92        Ok(())
93    }
94
95    /// No-op when already at root (`depth == 0`). Does not navigate anywhere after resetting.
96    ///
97    /// # Errors
98    ///
99    /// Returns [`MapError`] if any backup SETPATH fails to encode, the transport closes, or the
100    /// server returns a non-OK response.
101    pub(crate) async fn reset_to_root(&mut self) -> Result<(), MapError> {
102        for _ in 0..self.depth {
103            self.setpath_up().await?;
104        }
105        Ok(())
106    }
107
108    /// If already inside a subfolder (e.g. after a prior `set_folder` call), backs up to root
109    /// first, then navigates `telecom` → `msg` → folder. iOS requires one SETPATH per level;
110    /// a single slash-joined path is rejected.
111    ///
112    /// # Errors
113    ///
114    /// Returns [`MapError`] if any SETPATH fails to encode, the transport closes, or the
115    /// server returns a non-OK response for any step.
116    pub async fn set_folder(&mut self, folder: Folder) -> Result<(), MapError> {
117        self.reset_to_root().await?;
118        for segment in ["telecom", "msg", folder.as_str()] {
119            self.setpath(segment).await?;
120        }
121        Ok(())
122    }
123
124    /// Caller must navigate to the target folder via [`Self::set_folder`] before calling this.
125    /// Sends a `GetMessagesListing` GET with no Name header — the device lists the current OBEX
126    /// working directory. Accumulates body chunks across CONTINUE responses before parsing.
127    ///
128    /// # Errors
129    ///
130    /// Returns [`MapError`] if the transport fails, the server rejects the request, or the
131    /// response XML is malformed.
132    pub async fn list_messages(
133        &mut self,
134        filter: &ListMessagesFilter,
135    ) -> Result<Vec<MessageEntry>, MapError> {
136        let req = self.obex.get_request(
137            b"x-bt/MAP-msg-listing\x00",
138            None,
139            Some(filter.to_app_params()?),
140        )?;
141        self.transport.send(req).await?;
142        let body = self.collect_body().await?;
143        Ok(crate::xml::parse_message_listing(&body)?)
144    }
145
146    /// Sends a `GetMessage` GET with `Type: x-bt/message` and `Charset=UTF-8`. Accumulates body
147    /// chunks across CONTINUE responses before parsing.
148    ///
149    /// # Errors
150    ///
151    /// Returns [`MapError::InvalidInput`] if `handle` is empty or contains CR or LF — it lands
152    /// verbatim in the OBEX Name header. Returns [`MapError`] if the transport fails, the server
153    /// rejects the request, the response body exceeds 4 MiB, is not valid UTF-8, or the bMessage
154    /// is malformed.
155    pub async fn get_message(&mut self, handle: &str) -> Result<BMessage, MapError> {
156        if handle.is_empty() {
157            return Err(MapError::InvalidInput("handle must not be empty"));
158        }
159        if handle.contains(['\r', '\n']) {
160            return Err(MapError::InvalidInput("handle must not contain CR or LF"));
161        }
162        let req =
163            self.obex.get_request(b"x-bt/message\x00", Some(handle), Some(get_message_params()))?;
164        self.transport.send(req).await?;
165        let body = self.collect_body().await?;
166        let text = std::str::from_utf8(&body).map_err(|_| MapError::InvalidEncoding)?;
167        Ok(BMessage::parse(text)?)
168    }
169
170    /// Sends `text` as an outbound SMS to `phone` via MAP `PushMessage`. Returns the opaque
171    /// handle assigned by the remote, suitable for passing to `set_message_status`. Caller must
172    /// have navigated to outbox via `set_folder(Folder::Outbox)` first.
173    ///
174    /// # Errors
175    ///
176    /// Returns [`MapError`] if encoding fails, the transport closes, the server rejects the
177    /// request, or the OK response contains no Name header.
178    pub async fn push_message(&mut self, phone: &str, text: &str) -> Result<String, MapError> {
179        if phone.contains(['\r', '\n']) {
180            return Err(MapError::InvalidInput("phone must not contain CR or LF"));
181        }
182        let body = BMessage::outbound_sms(phone, text).encode();
183        let body_bytes = body.as_bytes();
184        let len = u32::try_from(body_bytes.len()).map_err(|_| ObexError::BodyTooLarge)?;
185        let req = self.obex.put_final_request(
186            b"x-bt/message\x00",
187            vec![
188                Header::Name(String::new()),
189                Header::Length(len),
190                Header::AppParams(push_message_params()),
191                Header::EndOfBody(Bytes::copy_from_slice(body_bytes)),
192            ],
193        )?;
194        self.transport.send(req).await?;
195        let rsp_bytes = Self::recv(&mut self.transport).await?;
196        let rsp = ObexClient::parse_response(&rsp_bytes)?;
197        if !rsp.opcode.is_ok() {
198            return Err(MapError::ServerError(rsp.opcode.to_byte()));
199        }
200        rsp.header_name().ok_or(MapError::MissingHandle)
201    }
202
203    /// Marks the message identified by `handle` as `status` (read or unread) via MAP
204    /// `SetMessageStatus`. Caller must navigate to the containing folder via `set_folder` first.
205    ///
206    /// # Errors
207    ///
208    /// Returns [`MapError::InvalidInput`] if `handle` is empty or contains CR or LF.
209    /// Returns [`MapError::ServerError`] if the remote returns a non-OK response.
210    /// Returns [`MapError::Obex`] or [`MapError::Transport`] on lower-layer failure.
211    pub async fn set_message_status_read(
212        &mut self,
213        handle: &str,
214        status: MessageStatus,
215    ) -> Result<(), MapError> {
216        let value = match status {
217            MessageStatus::Read => 0x01_u8,
218            MessageStatus::Unread => 0x00_u8,
219        };
220        self.do_set_message_status(handle, INDICATOR_READ_STATUS, value).await
221    }
222
223    /// Marks the message identified by `handle` as deleted (`true`) or undeleted (`false`) via
224    /// MAP `SetMessageStatus`. Caller must navigate to the containing folder via `set_folder` first.
225    ///
226    /// # Errors
227    ///
228    /// Returns [`MapError::InvalidInput`] if `handle` is empty or contains CR or LF.
229    /// Returns [`MapError::ServerError`] if the remote returns a non-OK response.
230    /// Returns [`MapError::Obex`] or [`MapError::Transport`] on lower-layer failure.
231    pub async fn set_message_status_deleted(
232        &mut self,
233        handle: &str,
234        deleted: bool,
235    ) -> Result<(), MapError> {
236        self.do_set_message_status(handle, INDICATOR_DELETED_STATUS, u8::from(deleted)).await
237    }
238
239    /// Returns the MAP folder listing for the current object store level via `GetFolderListing`
240    /// GET with Type `x-obex/folder-listing`. Folders are in device-reported document order.
241    ///
242    /// # Errors
243    ///
244    /// Returns [`MapError::ServerError`] if the remote returns a non-OK response.
245    /// Returns [`MapError::FolderListing`] if the response body is malformed XML.
246    /// Returns [`MapError::Obex`] or [`MapError::Transport`] on lower-layer failure.
247    pub async fn get_folder_listing(&mut self) -> Result<FolderListing, MapError> {
248        let req = self.obex.get_request(b"x-obex/folder-listing\x00", None, None)?;
249        self.transport.send(req).await?;
250        let body = self.collect_body().await?;
251        Ok(FolderListing::parse(&body)?)
252    }
253
254    /// When `enable` is `true`, the phone will connect to the MNS channel and push event reports.
255    /// When `false`, it stops. The caller must keep the MAP session alive while notifications are active.
256    ///
257    /// # Errors
258    ///
259    /// Returns [`MapError::ServerError`] if the remote returns a non-OK response.
260    /// Returns [`MapError::Obex`] or [`MapError::Transport`] on lower-layer failure.
261    pub async fn set_notification_registration(&mut self, enable: bool) -> Result<(), MapError> {
262        let req = self.obex.put_final_request(
263            b"x-bt/MAP-NotificationRegistration\x00",
264            vec![
265                Header::AppParams(set_notification_registration_params(enable)),
266                Header::EndOfBody(Bytes::new()),
267            ],
268        )?;
269        self.transport.send(req).await?;
270        let rsp_bytes = Self::recv(&mut self.transport).await?;
271        let rsp = ObexClient::parse_response(&rsp_bytes)?;
272        if !rsp.opcode.is_ok() {
273            return Err(MapError::ServerError(rsp.opcode.to_byte()));
274        }
275        Ok(())
276    }
277
278    async fn do_set_message_status(
279        &mut self,
280        handle: &str,
281        indicator: u8,
282        value: u8,
283    ) -> Result<(), MapError> {
284        if handle.is_empty() {
285            return Err(MapError::InvalidInput("handle must not be empty"));
286        }
287        if handle.contains(['\r', '\n']) {
288            return Err(MapError::InvalidInput("handle must not contain CR or LF"));
289        }
290        let req = self.obex.put_final_request(
291            b"x-bt/messageStatus\x00",
292            vec![
293                Header::Name(handle.to_owned()),
294                Header::AppParams(Bytes::from(set_message_status_params(indicator, value))),
295            ],
296        )?;
297        self.transport.send(req).await?;
298        let rsp_bytes = Self::recv(&mut self.transport).await?;
299        let rsp = ObexClient::parse_response(&rsp_bytes)?;
300        if !rsp.opcode.is_ok() {
301            return Err(MapError::ServerError(rsp.opcode.to_byte()));
302        }
303        Ok(())
304    }
305
306    async fn collect_body(&mut self) -> Result<Vec<u8>, MapError> {
307        let mut body = Vec::with_capacity(512);
308        loop {
309            let rsp_bytes = Self::recv(&mut self.transport).await?;
310            let rsp = ObexClient::parse_response(&rsp_bytes)?;
311            if rsp.opcode.is_continue() {
312                if let Some(chunk) = rsp.body_payload() {
313                    let new_len =
314                        body.len().checked_add(chunk.len()).ok_or(MapError::ResponseTooLarge)?;
315                    if new_len > MAX_BODY_BYTES {
316                        return Err(MapError::ResponseTooLarge);
317                    }
318                    body.extend_from_slice(chunk);
319                }
320                let cont = self.obex.get_continue_request()?;
321                self.transport.send(cont).await?;
322            } else if rsp.opcode.is_ok() {
323                if let Some(chunk) = rsp.body_payload() {
324                    let new_len =
325                        body.len().checked_add(chunk.len()).ok_or(MapError::ResponseTooLarge)?;
326                    if new_len > MAX_BODY_BYTES {
327                        return Err(MapError::ResponseTooLarge);
328                    }
329                    body.extend_from_slice(chunk);
330                }
331                break;
332            } else {
333                return Err(MapError::ServerError(rsp.opcode.to_byte()));
334            }
335        }
336        Ok(body)
337    }
338
339    /// Reads from the transport until the remote closes the stream, discarding all received
340    /// packets. Returns `Ok(())` on clean close. Does not send OBEX DISCONNECT and does not
341    /// parse received packet opcodes.
342    ///
343    /// # Errors
344    ///
345    /// Returns [`MapError::Transport`] on a framing error from the underlying codec.
346    pub async fn hold(&mut self) -> Result<(), MapError> {
347        loop {
348            match self.transport.next().await {
349                None => return Ok(()),
350                Some(Ok(_)) => {}
351                Some(Err(e)) => return Err(MapError::Transport(e)),
352            }
353        }
354    }
355
356    /// Sends OBEX DISCONNECT and awaits the server acknowledgement.
357    ///
358    /// Consumes `self` — the session is unusable after this call regardless of the outcome.
359    /// Does not close the underlying stream; the stream is dropped when `self` is consumed.
360    ///
361    /// # Errors
362    ///
363    /// Returns [`MapError`] if the request cannot be encoded, the transport fails, or the
364    /// server returns a non-OK response.
365    pub async fn disconnect(mut self) -> Result<(), MapError> {
366        let req = self.obex.disconnect_request()?;
367        self.transport.send(req).await?;
368        let rsp_bytes = Self::recv(&mut self.transport).await?;
369        let rsp = ObexClient::parse_response(&rsp_bytes)?;
370        if !rsp.opcode.is_ok() {
371            return Err(MapError::ServerError(rsp.opcode.to_byte()));
372        }
373        Ok(())
374    }
375
376    async fn recv(transport: &mut ObexTransport<T>) -> Result<Bytes, MapError> {
377        transport.next().await.ok_or(MapError::UnexpectedEof)?.map_err(MapError::Transport)
378    }
379}