sms_types/
sms.rs

1//! Generic types that apply to both HTTP and Websocket interfaces.
2
3use serde::{Deserialize, Serialize};
4
5/// Represents a stored SMS message from the database.
6#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
7pub struct SmsMessage {
8    /// Unique identifier for the message.
9    pub message_id: Option<i64>,
10
11    /// The phone number associated with this message.
12    pub phone_number: String,
13
14    /// The actual text content of the message.
15    pub message_content: String,
16
17    /// Optional reference number for message tracking.
18    /// This is assigned by the modem and is only present for outgoing messages.
19    pub message_reference: Option<u8>,
20
21    /// Whether this message was sent (true) or received (false).
22    pub is_outgoing: bool,
23
24    /// Unix timestamp when the message was created.
25    pub created_at: Option<u32>,
26
27    /// Optional Unix timestamp when the message was completed/delivered.
28    pub completed_at: Option<u32>,
29
30    /// Service message center delivery status.
31    pub status: Option<u8>,
32}
33impl SmsMessage {
34    /// Returns a clone of the message with the `message_id` option replaced.
35    #[must_use]
36    pub fn with_message_id(&self, id: Option<i64>) -> Self {
37        Self {
38            message_id: id,
39            ..self.clone()
40        }
41    }
42
43    /// Get the message created_at time as SystemTime.
44    #[must_use]
45    pub fn created_at(&self) -> Option<std::time::SystemTime> {
46        self.created_at
47            .map(|ts| std::time::UNIX_EPOCH + std::time::Duration::from_secs(u64::from(ts)))
48    }
49}
50
51/// The outgoing SMS message to be sent to a target number.
52#[derive(Serialize, PartialEq, Default, Debug, Clone)]
53pub struct SmsOutgoingMessage {
54    /// The target phone number, this should be in international format.
55    pub to: String,
56
57    /// The full message content. This will be split into multiple messages
58    /// by the server if required. This also supports Unicode emojis etc.
59    pub content: String,
60
61    /// The relative validity period to use for message sending. This determines
62    /// how long the message should remain waiting while undelivered.
63    /// By default, this is determined by the server (24 hours).
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub validity_period: Option<u8>,
66
67    /// Should the SMS message be sent as a Silent class? This makes a popup
68    /// show on the users device with the message content if they're logged in.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub flash: Option<bool>,
71
72    /// A timeout that should be applied to the entire request.
73    /// If one is not set, the default timeout is used.
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub timeout: Option<u32>,
76}
77impl SmsOutgoingMessage {
78    /// Create a new outgoing message with a default validity period and no flash.
79    /// The default validity period is applied by SMS-API, so usually 24 hours.
80    pub fn simple_message(to: impl Into<String>, content: impl Into<String>) -> Self {
81        Self {
82            to: to.into(),
83            content: content.into(),
84            ..Default::default()
85        }
86    }
87
88    /// Set the message flash state. This will show a popup if the recipient is
89    /// logged-in to their phone, otherwise as a normal text message.
90    #[must_use]
91    pub fn with_flash(mut self, flash: bool) -> Self {
92        self.flash = Some(flash);
93        self
94    }
95
96    /// Set a relative validity period value.
97    #[must_use]
98    pub fn with_validity_period(mut self, period: u8) -> Self {
99        self.validity_period = Some(period);
100        self
101    }
102
103    /// Set a request timeout value.
104    #[must_use]
105    pub fn with_timeout(mut self, timeout: u32) -> Self {
106        self.timeout = Some(timeout);
107        self
108    }
109
110    /// Get the message sending validity period, either as set or default.
111    /// Returns class 0 for a flash message.
112    #[must_use]
113    pub fn get_validity_period(&self) -> u8 {
114        if self.flash.unwrap_or(false) {
115            return 0;
116        }
117        self.validity_period.unwrap_or(167) // 24hr
118    }
119}
120impl From<&SmsOutgoingMessage> for SmsMessage {
121    fn from(outgoing: &SmsOutgoingMessage) -> Self {
122        SmsMessage {
123            message_id: None,
124            phone_number: outgoing.to.clone(),
125            message_content: outgoing.content.clone(),
126            message_reference: None,
127            is_outgoing: true,
128            status: None,
129            created_at: None,
130            completed_at: None,
131        }
132    }
133}
134
135/// An incoming message from the Modem.
136#[derive(Debug, Clone)]
137pub struct SmsIncomingMessage {
138    /// The incoming sender address. This could also be an alphanumeric sender name.
139    /// This is usually for registered businesses or carrier messages.
140    pub phone_number: String,
141
142    /// The decoded multipart header.
143    pub user_data_header: Option<SmsMultipartHeader>,
144
145    /// The raw message content.
146    pub content: String,
147}
148impl From<&SmsIncomingMessage> for SmsMessage {
149    fn from(incoming: &SmsIncomingMessage) -> Self {
150        SmsMessage {
151            message_id: None,
152            phone_number: incoming.phone_number.clone(),
153            message_content: incoming.content.clone(),
154            message_reference: None,
155            is_outgoing: false,
156            status: None,
157            created_at: None,
158            completed_at: None,
159        }
160    }
161}
162
163/// A received or stored delivery report.
164#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Copy)]
165#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
166pub struct SmsDeliveryReport {
167    /// Unique identifier for this delivery report.
168    pub report_id: Option<i64>,
169
170    /// Delivery status code from the network.
171    pub status: u8,
172
173    /// Whether this is the final delivery report for the message.
174    pub is_final: bool,
175
176    /// Unix timestamp when this report was created.
177    pub created_at: Option<u32>,
178}
179
180/// A partial message delivery report, as it comes from the modem.
181#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
182pub struct SmsPartialDeliveryReport {
183    /// The target phone number that received the message (and has now sent back a delivery report).
184    pub phone_number: String,
185    /// The modem assigned message reference, this is basically useless outside short-term tracking
186    /// the `message_id` is unique should always be used instead for identification.
187    pub reference_id: u8,
188
189    /// The SMS TP-Status: <https://www.etsi.org/deliver/etsi_ts/123000_123099/123040/16.00.00_60/ts_123040v160000p.pdf#page=71>
190    pub status: u8,
191}
192
193/// A general category of status message delivery status reports.
194#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
195pub enum SmsDeliveryReportStatusCategory {
196    /// The message has been sent, however not yet delivered.
197    Sent,
198
199    /// The message has been delivered.
200    Received,
201
202    /// The message has a temporary error, and sending will be retried by the carrier.
203    Retrying,
204
205    /// The message has a permanent error, the message will not be retried.
206    Failed,
207}
208impl From<u8> for SmsDeliveryReportStatusCategory {
209    fn from(value: u8) -> Self {
210        match value {
211            0x00 => SmsDeliveryReportStatusCategory::Received, // Received by SME
212            0x01..=0x02 => SmsDeliveryReportStatusCategory::Sent, // Forwarded/Replaced
213            0x03..=0x1F => SmsDeliveryReportStatusCategory::Sent, // Reserved/SC-specific success
214            0x20..=0x3F => SmsDeliveryReportStatusCategory::Retrying,
215            0x40..=0x6F => SmsDeliveryReportStatusCategory::Failed,
216            _ => SmsDeliveryReportStatusCategory::Failed,
217        }
218    }
219}
220impl std::fmt::Display for SmsDeliveryReportStatusCategory {
221    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222        f.write_str(match self {
223            SmsDeliveryReportStatusCategory::Sent => "Sent",
224            SmsDeliveryReportStatusCategory::Received => "Received",
225            SmsDeliveryReportStatusCategory::Retrying => "Retrying",
226            SmsDeliveryReportStatusCategory::Failed => "Failed",
227        })
228    }
229}
230impl From<&SmsDeliveryReport> for SmsDeliveryReportStatusCategory {
231    fn from(value: &SmsDeliveryReport) -> Self {
232        SmsDeliveryReportStatusCategory::from(value.status)
233    }
234}
235impl From<&SmsPartialDeliveryReport> for SmsDeliveryReportStatusCategory {
236    fn from(value: &SmsPartialDeliveryReport) -> Self {
237        SmsDeliveryReportStatusCategory::from(value.status)
238    }
239}
240
241/// The sms message multipart header.
242#[derive(Debug, Clone, Copy)]
243pub struct SmsMultipartHeader {
244    /// Modem assigned message send reference (overflows).
245    pub message_reference: u8,
246
247    /// The total amount of messages within this multipart.
248    pub total: u8,
249
250    /// The current received message index.
251    pub index: u8,
252}
253impl TryFrom<Vec<u8>> for SmsMultipartHeader {
254    type Error = &'static str;
255
256    fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
257        if data.len() != 3 {
258            return Err("Invalid user data length!");
259        }
260        Ok(Self {
261            message_reference: data[0],
262            total: data[1],
263            index: data[2],
264        })
265    }
266}