Skip to main content

imsg_map/
params.rs

1//! MAP application parameter tag constants, bitflags, and encoder functions.
2
3use bitflags::bitflags;
4use bytes::Bytes;
5
6use crate::MapError;
7
8bitflags! {
9    /// `SupportedMessageTypes` bitmask from `GetMASInstanceInformation`.
10    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
11    pub struct MessageTypes: u8 {
12        /// GSM cellular SMS messages.
13        const SMS_GSM  = 0x01;
14        /// CDMA cellular SMS messages.
15        const SMS_CDMA = 0x02;
16        /// Email messages.
17        const EMAIL    = 0x04;
18        /// MMS multimedia messages.
19        const MMS      = 0x08;
20        /// Instant messages (e.g. RCS, IM).
21        const IM       = 0x10;
22    }
23}
24
25bitflags! {
26    /// `FilterMessageType` application parameter bitmask for `GetMessagesListing`.
27    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
28    pub struct FilterMessageType: u8 {
29        /// Include GSM SMS messages in results.
30        const SMS_GSM  = 0x01;
31        /// Include CDMA SMS messages in results.
32        const SMS_CDMA = 0x02;
33        /// Include email messages in results.
34        const EMAIL    = 0x04;
35        /// Include MMS messages in results.
36        const MMS      = 0x08;
37    }
38}
39
40/// MAP Application Parameter tag constants.
41pub mod tag {
42    /// If set, message is not copied to the sent folder.
43    pub const TRANSPARENT: u8 = 0x0A;
44    /// If set, retry delivery on transient failure.
45    pub const RETRY: u8 = 0x0B;
46    /// Character set (0x01 = UTF-8).
47    pub const CHARSET: u8 = 0x14;
48    /// Bitmask — see `FilterMessageType`.
49    pub const FILTER_MESSAGE_TYPE: u8 = 0x03;
50    /// Response-only: total folder count available.
51    pub const FOLDER_LISTING_SIZE: u8 = 0x07;
52    pub(crate) const MAX_LIST_COUNT: u8 = 0x01;
53    pub(crate) const LIST_START_OFFSET: u8 = 0x02;
54    /// Response-only: 1 if unread messages exist.
55    pub const NEW_MESSAGE: u8 = 0x0D;
56    /// 1 = enable MNS event reports, 0 = disable.
57    pub const NOTIFICATION_STATUS: u8 = 0x0E;
58    pub(crate) const FILTER_PERIOD_BEGIN: u8 = 0x04;
59    pub(crate) const FILTER_PERIOD_END: u8 = 0x05;
60    pub(crate) const FILTER_READ_STATUS: u8 = 0x06;
61    pub(crate) const FILTER_ORIGINATOR: u8 = 0x08;
62    /// Response-only: total message count available (MAP 1.4 wire tag 0x12).
63    pub const MESSAGES_LISTING_SIZE: u8 = 0x12;
64    /// Selects which status property to update in `SetMessageStatus`.
65    pub const STATUS_INDICATOR: u8 = 0x17;
66    /// The new value for the selected status property in `SetMessageStatus`.
67    pub const STATUS_VALUE: u8 = 0x18;
68}
69
70/// Charset value for MAP `PushMessage` (UTF-8).
71pub const CHARSET_UTF8: u8 = 0x01;
72
73/// `StatusIndicator` wire value selecting read/unread state.
74pub const INDICATOR_READ_STATUS: u8 = 0x00;
75/// `StatusIndicator` wire value selecting deleted/present state.
76pub const INDICATOR_DELETED_STATUS: u8 = 0x01;
77
78/// Always `[0x14, 0x01, 0x01]`.
79#[must_use]
80pub const fn push_message_params() -> Bytes {
81    Bytes::from_static(b"\x14\x01\x01")
82}
83
84/// Always `[0x14, 0x01, 0x01]`.
85#[must_use]
86pub const fn get_message_params() -> Bytes {
87    Bytes::from_static(b"\x14\x01\x01")
88}
89
90/// 6-byte APP-PARAMS: `StatusIndicator` (tag `0x17`, 1 byte) + `StatusValue` (tag `0x18`, 1 byte).
91#[must_use]
92pub fn set_message_status_params(indicator: u8, value: u8) -> Vec<u8> {
93    let mut params = Vec::with_capacity(6);
94    params.extend_from_slice(&[tag::STATUS_INDICATOR, 0x01, indicator]);
95    params.extend_from_slice(&[tag::STATUS_VALUE, 0x01, value]);
96    params
97}
98
99/// Encodes `NotificationStatus` (tag `0x0E`, 1 byte) + `MasInstanceId` (tag `0x0F`, 1 byte).
100#[must_use]
101pub const fn set_notification_registration_params(enable: bool) -> Bytes {
102    if enable {
103        Bytes::from_static(b"\x0e\x01\x01\x0f\x01\x00")
104    } else {
105        Bytes::from_static(b"\x0e\x01\x00\x0f\x01\x00")
106    }
107}
108
109/// Builds a MAP app-params blob for `ListMessages`.
110///
111/// Encodes `MaxListCount` and `ListStartOffset` unconditionally. Appends `FilterReadStatus` when
112/// `read_status` is `Some`. String filters are encoded as null-terminated UTF-8 with a 1-byte
113/// length prefix (`[tag][len = bytes+1][UTF-8][0x00]`).
114///
115/// # Errors
116///
117/// Returns [`MapError::InvalidInput`] if any string contains a null byte or exceeds 254 UTF-8 bytes.
118pub fn list_messages_params(
119    max_count: u16,
120    offset: u16,
121    read_status: Option<u8>,
122    originating_address: Option<&str>,
123    period_begin: Option<&str>,
124    period_end: Option<&str>,
125) -> Result<Vec<u8>, MapError> {
126    let mut cap = 8_usize;
127    if read_status.is_some() {
128        cap = cap.saturating_add(3);
129    }
130    for s in [originating_address, period_begin, period_end].into_iter().flatten() {
131        cap = cap.saturating_add(s.len().saturating_add(3));
132    }
133    let mut params = Vec::with_capacity(cap);
134    params.extend_from_slice(&[tag::MAX_LIST_COUNT, 0x02]);
135    params.extend_from_slice(&max_count.to_be_bytes());
136    params.extend_from_slice(&[tag::LIST_START_OFFSET, 0x02]);
137    params.extend_from_slice(&offset.to_be_bytes());
138    if let Some(status) = read_status {
139        params.extend_from_slice(&[tag::FILTER_READ_STATUS, 0x01, status]);
140    }
141    push_str_tlv(&mut params, tag::FILTER_ORIGINATOR, originating_address)?;
142    push_str_tlv(&mut params, tag::FILTER_PERIOD_BEGIN, period_begin)?;
143    push_str_tlv(&mut params, tag::FILTER_PERIOD_END, period_end)?;
144    Ok(params)
145}
146
147fn push_str_tlv(out: &mut Vec<u8>, tag_byte: u8, value: Option<&str>) -> Result<(), MapError> {
148    let Some(s) = value else {
149        return Ok(());
150    };
151    if s.contains('\x00') {
152        return Err(MapError::InvalidInput("filter string must not contain null bytes"));
153    }
154    let len = u8::try_from(s.len().saturating_add(1))
155        .map_err(|_| MapError::InvalidInput("filter string exceeds 254 UTF-8 bytes"))?;
156    out.push(tag_byte);
157    out.push(len);
158    out.extend_from_slice(s.as_bytes());
159    out.push(0x00);
160    Ok(())
161}
162
163#[cfg(test)]
164mod tests {
165    use super::list_messages_params;
166    use crate::MapError;
167
168    #[test]
169    fn list_messages_params_with_originator() -> Result<(), MapError> {
170        let bytes = list_messages_params(1024, 0, None, Some("5550001001"), None, None)?;
171        let tlv: &[u8] = b"\x08\x0b5550001001\x00";
172        assert!(bytes.windows(tlv.len()).any(|w| w == tlv));
173        Ok(())
174    }
175
176    #[test]
177    fn list_messages_params_with_period_filters() -> Result<(), MapError> {
178        let bytes = list_messages_params(
179            1024,
180            0,
181            None,
182            None,
183            Some("20260601T000000"),
184            Some("20260605T235959"),
185        )?;
186        let begin_tlv: &[u8] = b"\x04\x1020260601T000000\x00";
187        let end_tlv: &[u8] = b"\x05\x1020260605T235959\x00";
188        assert!(bytes.windows(begin_tlv.len()).any(|w| w == begin_tlv));
189        assert!(bytes.windows(end_tlv.len()).any(|w| w == end_tlv));
190        Ok(())
191    }
192
193    #[test]
194    fn list_messages_params_rejects_null_byte() {
195        let result = list_messages_params(1024, 0, None, Some("abc\x00def"), None, None);
196        assert!(matches!(result, Err(MapError::InvalidInput(_))));
197    }
198
199    #[test]
200    fn list_messages_params_rejects_overlong() {
201        let long: String = "a".repeat(255);
202        let result = list_messages_params(1024, 0, None, Some(&long), None, None);
203        assert!(matches!(result, Err(MapError::InvalidInput(_))));
204    }
205}