Skip to main content

android_sms_gateway/types/
message.rs

1use std::collections::HashMap;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6/// The processing state of a message.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub enum ProcessingState {
9    #[serde(rename = "Pending")]
10    Pending,
11    #[serde(rename = "Cancelling")]
12    Cancelling,
13    #[serde(rename = "Cancelled")]
14    Cancelled,
15    #[serde(rename = "Processed")]
16    Processed,
17    #[serde(rename = "Sent")]
18    Sent,
19    #[serde(rename = "Delivered")]
20    Delivered,
21    #[serde(rename = "Failed")]
22    Failed,
23}
24
25/// Message priority value. Messages with priority > 99 bypass limits and delays.
26pub type MessagePriority = i8;
27
28/// Minimum priority value.
29pub const PRIORITY_MINIMUM: MessagePriority = -128;
30/// Default priority value.
31pub const PRIORITY_DEFAULT: MessagePriority = 0;
32/// Threshold at which messages bypass limits and delays.
33pub const PRIORITY_BYPASS_THRESHOLD: MessagePriority = 100;
34/// Maximum priority value.
35pub const PRIORITY_MAXIMUM: MessagePriority = 127;
36
37/// A text message payload.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct TextMessage {
40    /// The message text content.
41    pub text: String,
42}
43
44/// A binary data message payload.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct DataMessage {
47    /// Base64-encoded binary data.
48    pub data: String,
49    /// The destination port number.
50    pub port: u16,
51}
52
53/// A single attachment of an MMS message.
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct MmsAttachment {
57    /// The MIME type of the attachment (e.g. `image/png`).
58    pub content_type: String,
59    /// The optional file name of the attachment.
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub name: Option<String>,
62    /// Base64-encoded attachment content.
63    pub data: String,
64}
65
66/// An MMS message payload.
67///
68/// At least one of `text` or `attachments` must be set.
69#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
70#[serde(rename_all = "camelCase")]
71pub struct MmsMessage {
72    /// The optional subject of the MMS.
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub subject: Option<String>,
75    /// The optional text body of the MMS.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub text: Option<String>,
78    /// The list of attachments. Omitted when empty.
79    #[serde(default, skip_serializing_if = "Vec::is_empty")]
80    pub attachments: Vec<MmsAttachment>,
81}
82
83/// A reference to a hashed message (content not included for privacy).
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct HashedMessage {
86    /// SHA256 hash of the message content.
87    pub hash: String,
88}
89
90/// A message to be sent.
91///
92/// At least one of `text_message`, `data_message`, or `mms_message`
93/// must be set.
94#[derive(Debug, Clone, Default, Serialize, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub struct Message {
97    /// Optional message ID (generated if not provided).
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub id: Option<String>,
100    /// Optional device ID for explicit device selection.
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub device_id: Option<String>,
103
104    /// Deprecated: use `text_message` instead.
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    pub message: Option<String>,
107
108    /// Text message payload.
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub text_message: Option<TextMessage>,
111    /// Data message payload.
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub data_message: Option<DataMessage>,
114    /// MMS message payload.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub mms_message: Option<MmsMessage>,
117
118    /// List of recipient phone numbers.
119    pub phone_numbers: Vec<String>,
120    /// Whether the message content is encrypted.
121    #[serde(default)]
122    pub is_encrypted: bool,
123
124    /// SIM card number to use (1-3). Uses default SIM if not set.
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub sim_number: Option<u8>,
127    /// Whether to request a delivery report.
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub with_delivery_report: Option<bool>,
130    /// Message priority. Values > 99 bypass limits and delays.
131    #[serde(default)]
132    pub priority: MessagePriority,
133
134    /// Time-to-live in seconds (conflicts with `valid_until`).
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub ttl: Option<u64>,
137    /// Time until the message is valid (conflicts with `ttl`).
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub valid_until: Option<DateTime<Utc>>,
140    /// Schedule message delivery at this time (must be in the future).
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub schedule_at: Option<DateTime<Utc>>,
143}
144
145impl Message {
146    /// Returns the text message content, either from `text_message` or
147    /// the deprecated `message` field.
148    pub fn get_text_message(&self) -> Option<TextMessage> {
149        if let Some(ref tm) = self.text_message {
150            return Some(tm.clone());
151        }
152        self.message
153            .as_ref()
154            .filter(|m| !m.is_empty())
155            .map(|m| TextMessage { text: m.clone() })
156    }
157
158    /// Returns the data message payload, if set.
159    pub fn get_data_message(&self) -> Option<&DataMessage> {
160        self.data_message.as_ref()
161    }
162
163    /// Returns the MMS message payload, if set.
164    pub fn get_mms_message(&self) -> Option<&MmsMessage> {
165        self.mms_message.as_ref()
166    }
167
168    /// Validates the message structure.
169    ///
170    /// Checks that exactly one content type is set and that no conflicting
171    /// fields (`ttl` + `valid_until`) are provided.
172    pub fn validate(&self) -> Result<(), crate::Error> {
173        let filled = self
174            .message
175            .as_ref()
176            .map(|s| !s.is_empty())
177            .unwrap_or(false) as u8
178            + self.text_message.is_some() as u8
179            + self.data_message.is_some() as u8
180            + self.mms_message.is_some() as u8;
181
182        if filled == 0 {
183            return Err(crate::Error::Validation(
184                "must specify exactly one of: textMessage, dataMessage or mmsMessage".to_string(),
185            ));
186        }
187        if filled > 1 {
188            return Err(crate::Error::ConflictFields(
189                "must specify exactly one of: textMessage, dataMessage or mmsMessage".to_string(),
190            ));
191        }
192
193        if let Some(ref mms) = self.mms_message {
194            let has_text = mms.text.as_ref().map(|t| !t.is_empty()).unwrap_or(false);
195            let has_attachments = !mms.attachments.is_empty();
196            if !has_text && !has_attachments {
197                return Err(crate::Error::Validation(
198                    "mmsMessage must specify either text or at least one attachment".to_string(),
199                ));
200            }
201        }
202
203        if self.ttl.is_some() && self.valid_until.is_some() {
204            return Err(crate::Error::ConflictFields(
205                "ttl and validUntil".to_string(),
206            ));
207        }
208
209        if let Some(ref schedule_at) = self.schedule_at {
210            if *schedule_at <= Utc::now() {
211                return Err(crate::Error::Validation(
212                    "scheduleAt must be in the future".to_string(),
213                ));
214            }
215        }
216
217        Ok(())
218    }
219}
220
221/// The state of a single message recipient.
222#[derive(Debug, Clone, Serialize, Deserialize)]
223#[serde(rename_all = "camelCase")]
224pub struct RecipientState {
225    /// Phone number or first 16 chars of SHA256 hash.
226    pub phone_number: String,
227    /// Current processing state for this recipient.
228    pub state: ProcessingState,
229    /// Error message (present when `state` is `Failed`).
230    #[serde(default, skip_serializing_if = "Option::is_none")]
231    pub error: Option<String>,
232}
233
234/// The current state of a sent message.
235#[derive(Debug, Clone, Serialize, Deserialize)]
236#[serde(rename_all = "camelCase")]
237pub struct MessageState {
238    /// Message ID.
239    pub id: String,
240    /// Device ID that processed the message.
241    pub device_id: String,
242    /// Current processing state.
243    pub state: ProcessingState,
244    /// Whether the message content is hashed.
245    #[serde(default)]
246    pub is_hashed: bool,
247    /// Whether the message content is encrypted.
248    #[serde(default)]
249    pub is_encrypted: bool,
250    /// Per-recipient delivery states.
251    #[serde(default)]
252    pub recipients: Vec<RecipientState>,
253    /// History of state transitions.
254    #[serde(default)]
255    pub states: HashMap<String, DateTime<Utc>>,
256
257    /// Text message content (present when `includeContent=true`).
258    #[serde(default, skip_serializing_if = "Option::is_none")]
259    pub text_message: Option<TextMessage>,
260    /// Data message content (present when `includeContent=true`).
261    #[serde(default, skip_serializing_if = "Option::is_none")]
262    pub data_message: Option<DataMessage>,
263    /// MMS message content (present when `includeContent=true`).
264    #[serde(default, skip_serializing_if = "Option::is_none")]
265    pub mms_message: Option<MmsMessage>,
266    /// Hashed message reference (present when `is_hashed=true`).
267    #[serde(default, skip_serializing_if = "Option::is_none")]
268    pub hashed_message: Option<HashedMessage>,
269}
270
271impl MessageState {
272    /// Validates the state history entries.
273    pub fn validate(&self) -> Result<(), crate::Error> {
274        for key in self.states.keys() {
275            match key.as_str() {
276                "Pending" | "Cancelling" | "Cancelled" | "Processed" | "Sent" | "Delivered"
277                | "Failed" => {}
278                _ => {
279                    return Err(crate::Error::Validation(format!(
280                        "invalid state value: {}",
281                        key
282                    )));
283                }
284            }
285        }
286        Ok(())
287    }
288}